[React] 리액트 더 올바르게 이용해보기

mangsuyo·2024년 4월 28일
post-thumbnail

React란

React는 State와 Props를 가진 컴포넌트들의 집합체이다.

Virtual DOM을 이용하여, 실제로 변경된 부분만 브라우저 상에 업데이트 된다.


useState 올바르게 사용하기

UI가 업데이트 되는 것은 리액트의 상태를 통해 변경되어야 한다.
리액트는 UI의 업데이트를 상태로 통제하는 것을 좋아한다.

useState에게 전달되는 것이 원시 값이 아닌, 자료구조(배열, 객체, 함수)가 전달된다고 생각해보자!
자료구조 안의 속성을 바꾼다고 해서, 참조값은 변화하지 않는다!

불변성 : 어떤 값을 직접적으로 변경하지 않고 새로운 값을 만들어내는 것

불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 변화한 것을 감지하지 못한다

const [animal, setAnimal] = useState([
	{
		id:0,
		name: '호랑이',
		eat:[고기, 채소]
	}
])
animal의 이름을 '강아지'로 바꾸고 싶다면,
animal.name = '강아지'
setAnimal(animal)로 업데이트가 될까?
놀랍게도, 그렇지 않다.

useState는 값을 반환한다, 배열, 객체, 함수등은 레퍼런스(참조값)이 초기값으로 전달되었을 것이고 animal.name을 수정한다고 해서 참조값이 달라지는 것이 아니므로

업데이트는 발생하지 않고 리렌더링이 이뤄지지 않는다!


불변성 : 중첩 객체 상태 관리

const obj = {
	name: '명수',
	eat: {
		name: '고기',
		title: '정육왕',
	},
}
const [food, setFood] = useState(obj);
// 일정한 조건에서 food의 name을 변경
-> food.name = '생선' (X)
상태의 불변성을 유지해 주어야 한다.

Spread 연산자를 이용해보자
setFood({...food, eat: {...food.eat, name:'생선'}})(O)

Fragment

필요성

그동안 반환하는 두 개 이상의 요소는 항상 감싸져야 하는 요소로 wrapper 요소가 필요했습니다.

그러나 Fragment를 이용하면, 한 개 이상의 요소를 반환 할 수있으며, html에서 우리의 의도에 맞게 어떠한 wrapper요소 없이 요소들을 별개로 인식합니다.


// 두 개 이상의 요소
<>
  <OneChild />
  <AnotherChild />
</>

// 리스트 렌더링 (Key)
function Blog() {
  return posts.map(post =>
    <Fragment key={post.id}>
      <PostTitle title={post.title} />
      <PostBody body={post.body} />
    </Fragment>
  );
}

Portal

필요성

모달 창같이 body를 전체로 덮는 부분은 root와 같은 계층에 위치시키고 싶을 때가 있다.

위의 예시와 같이 다른 DOM에서 렌더링이 일어나길 기대하고 싶을 때가 있다.
그럴 때 이용할 수 있는게 Portal이다.


createPortal(children, DOMNode, key?)

첫번째 인자로 다른 곳에서 렌더링 시키고 싶은 컴포넌트(요소), 두번째 인자로 렌더링이 일어날 위치를 전달해준다.

// react-dom 라이브러리에서 createProtal을 관리한다.
import {createPortal} from 'react-dom'

export default function PortalExample() {
  const [showModal, setShowModal] = useState(false);
  return (
    <>
      <button onClick={() => setShowModal(true)}>
        Show modal using a portal
      </button>
      {showModal && createPortal(
        <ModalContent onClose={() => setShowModal(false)} />,
        document.body // rendering 시키고 싶은 위치
      )}
    </>
  );
}

유용한 리액트 Hooks


Hook : useRef

useRef는 상태를 조작하지 않고 값을 참조하고 싶을 때 이용할 수 있습니다.

보통 상태가 변경되면 리렌더링이 일어나기 때문에, 리렌더링을 원하지 않을 때 ref를 사용합니다.
scroll, focus등 직접 DOM을 조작해야만 하는 것들이 있습니다.


useRef( initialValue )

ref 객체를 반환하고, ref의 current라는 속성을 통해 값에 접근할 수 있습니다.

ref의 값이 변화해도, 리렌더링이 발생하지 않습니다.

import {useRef} from 'react';

const ref = useRef(0);
console.log(ref.current);

// DOM 조작
const nameInputRef = useRef();
const ageInputRef = useRef();

<input ref={nameInputRef} />
<input ref={ageInputRef} />

console.log(nameInputRef.current.value);
console.log(ageInputRef.current.value);
// 초기화
nameInputRef.current.value = "";
ageInputRef.current.value = "";

Hook : useEffect

필요성

const [data, setData] = useState([]);
fetch('apple')
.then((res) => res.json())
.then((data) => setData(data))

위의 코드는 무한루프가 발생한다.

함수나 객체 배열 등이 전달될 때 실제로 전달되는 것은 그것들의 레퍼런스(참조값)이다.

같은 값을 가진 배열들이 다른 메모리 주소를 가지고 있으므로, data는 계속해서 변화되는 것이다!
따라서, 특정 조건에만 렌더링을 가능하게 하는 훅이 필요하다.


useEffect는 콜백함수와 의존성배열을 전달받는 Hook입니다.

기본적으로, 의존성배열의 값이 변화할 때마다 콜백함수가 실행됩니다.
의존성 배열을 빈 배열로 전달하면, [] 바뀌지 않으므로, 최초 마운트가 되었을 때 호출이 한 번만 이뤄지게 됩니다.


 // 예시 1 : 로그인 이용
useEffect(() => {
	// 만약 로그인이 되어 있다면 isLoggedIn = true
	// 만약 로그인이 되어 있지 않다면 isLoggedIn = false
}, [])

// 예시 2 : 데이터 통신
const [data, setData] = useState([]);
useEffect(() => {
// 최초 1회만 실행됨
fetch('apple')
.then((res) => res.json())
.then((data) => setData(data))
// 언마운트 되었을 때, return이 실행되며, clean-up 과정이라 부른다.
// 언마운트는 그냥 함수실행이 끝나면 언마운트 된다.
return () => {
	console.log("언마운트 되었다!");
}
}, [])

// 예시 3 : 타이머 - clean Up
// 타이머라는 컴포넌트가 마운트되면 실행, 언마운트 되면? return 함수 실행
const Timer = (props) => {
    useEffect(() => {
      const timer = setInterval(() => {
        console.log("타이머 돌아가는중...");
      }, 1000);
      return () => {
        clearInterval(timer);
        console.log("타이머가 종료되었습니다.");
      }
    }, [])
  }

Debounce

과도한 요청, 처리를 수행하게 될 경우 발생할 수 있는 성능 저하를 막기 위해 이를 효과적으로 제어하는 방식 중 하나입니다.

일정 시간 안에 동일 이벤트가 발생하면 마지막 발생 시점부터 일정 시간을 지연하여 연속적으로 호출되는 함수 중 마지막 함수만 호출합니다.

const [text, setText] = useState("");
const SearchBar = () => {
	useEffect(() => {
		const timer = setTimeout(() => {
			isValid(text);	
		}, 100)
		return () => {
			clearTimeout(timer);
			console.log("이벤트가 0.1초 이내 재작동했습니다.");
		}}, [text])

Hook : useReducer

필요성

useState는 여러개의 하위값을 포함하는 복잡한 state를 다룰 때는 적절하지 않습니다.

또한, setState 함수를 계속해서 Props로 전달해줘야 하는 불편함이 있습니다.

useReducer는 여러개의 하위값을 포함하는 복잡한 State를 다룰 때나 다른 컴포넌트에서도 이러한 로직을 재사용할 수 있게 해줍니다.


구성요소

useReducer는 3개의 구성요소로 되어있습니다.

Dispatch, Reducer, Action

Dispatch 함수를 통해 우리가 Reducer에 요구하는 Action을 전달할 수 있습니다. (Action은 객체 타입으로 전달해줍니다.)

Reducer는 Action을 받은 후, Action이 요구하는 대로 State를 변경합니다.

// reducer - state를 업데이트 하는 역할 (은행)
// dispatch - state 업데이트를 위한 요구
// action - 요구의 내용

reducer.js
export default personReducer(state, action) {
	swtich(action.type){
		case "add"{
			const name = action.name 
			return{...person, mentor:[...person.mentor]}
		}
	}
}
App.jsx
import personReducer from "./reducer.js"
const initialState = {};
const [person, dispatch] = useReducer(personReducer, initialState)
const handleAdd = () => {
	const name = prompt("이름을 입력해주세요");
	dispatch({type: "add", name})
	보통 dispatch에 action은 객체 타입으로 전송합니다.
}

참고 : https://youtu.be/tdORpiegLg0


상태 관리 라이브러리 : Immer

불변성이었던 state를 가변성으로 다룰 수 있게 도와주는 라이브러리 입니다.

useState 이용시 불변성을 유지하기 위해, 하위 값을 변경시키려면, 모든 값을 
초기화 해야만 했습니다.
setPerson({
	...person,
	mentor:[
	...person.mentor,
	name : '내가 원하는 이름',
]})


Immer 이용시 state를 가변적으로 통제할 수 있습니다.
updatePerson({
	person.mentor.name = '내가 원하는 이름'	
})

동일 레벨의 컴포넌트간의 데이터 전달 방식

React는 동일 레벨의 컴포넌트 간에서의 데이터 교환은 직접적으로 불가능합니다.

그렇기 때문에, 상위 계층에서 props를 전달하거나, 하위 계층에서 전달받은 함수를 호출하여, 데이터를 전송하는 방식을 사용합니다.


Hook : useContext

필요성

멀리 떨어져있는 컴포넌트 간의 정보전달을 위해서 많은 컴포넌트들을 거쳐서 Props를 전달해주어야 합니다.

또한 언어, 테마(다크모드), 로그인정보와 같이 전역적으로 쓰이는 정보들을 공유하기 위해 모든 컴포넌트에 Props로 정보를 전달해야 할 것입니다.

그로인해 컴포넌트가 자기가 사용하지 않는 Props 일지라도 매개변수로 받고 이를 전달해주어야 합니다.

Context는 이를 해결해줍니다.

주의할 점

Context API로 관리하던 상태가 변경되면, 그것을 참조하는 모든 컴포넌트들이 리렌더링 되기 때문에, 잦은 변화가 일어나는 상태를 다루기에는 적합하지 않습니다.

정말 데이터의 전달이 필요한 곳의 상위요소에 적절히 우산(Context)을 씌어주는 것을 Umbrella Technic이라 부릅니다.


useContext 이용해보기

// DarkModeContext.jsx
import {createContext} from 'react';
export const DarkModeContext = createContext();
export function DarkModeProvider({children}){
	const [darkMode, setDarkMode] = useState(false);
	const toggleDarkMode = () => {
		setDarkMode(!darkMode);
	}
// value에는 전달하고 싶은 내용을 객체로 담아서
	return <DarkModeContext.Provider value={{darkMode, toggleDarkMode}}>{children}</DarkModeContext.Provider>
}

// App.jsx
<DarkModeProvider>
	<Main/>
</DarkModeProvider>

이제 DarkModeProvider 태그로 감싸진 모든 컴포넌트 혹은 그 하위요소들은
전달된 데이터를 이용할 수 있습니다.
const {darkMode, toggleDarkMode} = useContext(DarkModeContext);

Custom Hook

필요성

재사용이 필요한 로직을 매번 짜는 것은 비효율적입니다.

따라서, 복잡하고 자주 쓰이는 로직을 커스텀 훅으로 설정해서 다른 곳에서 사용할 수 있게 만듭시다.
컴포넌트와 다른 점은 retrun 값이 UI와 관련된 jsx가 아니라, 데이터의 전달에 있다는 것을 명심해야 겠습니다.

import { useEffect, useState } from 'react';

export default function useProducts({salesOnly}) {
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState();
    const [products, setProducts] = useState([]);
    useEffect(() => {
        setLoading(true);
        setError(undefined);
        fetch(`data/${salesOnly? 'sale_' : ''}products.json`)
        .then((res) => res.json())
        .then((data) => {
            console.log("뜨끈한 테이터를 네트워크에서 받아옴");
            setProducts(data);
        })
        .catch((e) => setError('에러가 발생했음!'))
        .finally(() => setLoading(false));
    return () => {
        console.log(`깨끗하게 청소하는 일들을 합니다.`);
    }
    },[salesOnly]);
    
    return [loading, error, products]
}
일반 컴포넌트는 jsx를 리턴하는 반면에, 커스텀 훅은 공유하고 싶은 데이터를 반환한다.
Hooks은(함수들은) 값의 재사용이 아니라 로직의 재사용을 위한 것이다.

LocalStorage 이용하기

localStorage는 웹 스토리지 객체로 브라우저 내 키-값 쌍으로 저장하도록 도와줍니다

localStorage를 사용하면 페이지를 새로 고침하거나 브라우저를 껐다 켜도 데이터가 사라지지 않고 해당 사용자에게 데이터가 남아있습니다.

아이템 등록하기

localStorage.setItem("datalist", data);
이렇게 아이템을 등록하면, data라는 value는 [Object Object] 형식으로 나오는데,
이는 storage에 저장된 데이터는 모두 문자열만 사용가능 하기 때문에,
JSON.stringfy()를 통해 객체를 문자열로 변환해 주어야 합니다.
// JSON.parse() : 문자열 -> 객체

localStorage.setItem("datalist", JSON.stringfy(data));

아이템 가져오기

JSON.parse(localStorage.getItem('datalist'))

적용하기

새로고침이 되어도, 리스트의 값을 유지하고 싶다 -> localStorage이용!
이미 내부적으로 state 구현이 끝난 상태,
useState의 초깃값을 localStorage에 있는 리스트로 할당, localStorage는 기존 state인 list가
변화할때(추가 혹은 삭제)만 업데이트! -> useEffect이용!

profile
한망수요

0개의 댓글