[Week 1-1] 전역변수를 설정하는 기준과 방법
-챌린지 강병진 멘토님-
https://github.com/jasonkang14
1-1) 지역변수와 전역변수 비교
- 지역 변수 (Local Variable)
함수 내부에서 선언된 변수 지역 변수는 해당 함수 내부에서만 접근할 수 있으며, 함수가 종료되면 메모리에서 사라진다. 다른 함수에서는 같은 이름의 지역 변수를 사용해도 각각 독립된 메모리 공간을 가지므로, 서로 영향을 미치지 않는다.
- 전역 변수 (Global Variable)
함수 외부, 최상위 레벨에서 선언된 변수를 말합니다. 전역 변수는 프로그램 어디에서나 접근할 수 있습니다. 따라서 주의해서 사용해야 합니다. 전역 변수를 과도하게 사용하면, 변수 이름이 충돌하거나 예상치 못한 데이터 변경 등의 문제가 발생할 수 있습니다.
지역 변수와 전역 변수는 변수의 유효 범위(scope)를 기준으로 분류하게 되는데, 함수 내부에서 선언된 변수는 함수 내부에서만 접근할 수있고 함수 외부나 최상위에 선언된 변수는 어디에서나 접근이 가능하다.
일반적으로 React 환경에서 개발을 할 때 가독성 및 모듈화를 위해 컴포넌트들을 나누어 놓는데 상위 컴포넌트에서 정의, 선언된 변수를 하위 컴포넌트에서 props로 받아 사용하게 된다.
이렇게 부모 컴포넌트에서 자식 컴포넌트로 계속해서 내려가다 보면 TodoItem 컴포넌트에선 toggle 함수를 사용하지 않더라도 props로 전달해야 하는 Props Drilling이 발생하게 된다.
이를 해결하기 위해 Context API나 Redux와 같은 상태 관리 라이브러리를 사용하게 된다.
1-2) 다양한 툴 비교
오늘 챌린지에선 전역 변수 라이브러리로 Context API, Redux, Recoil, Zustand를 소개해 주셨다.
Context API
https://ko.legacy.reactjs.org/docs/context.html
State를 Provide 하는 방식
React에 내장돼있기 때문에 설치 불필요
HTTP Request도 Context에서 관리 가능
비즈니스 로직이 복잡해지는 경우 많은 Provider 필요
Provider 하나만 생성할 경우에도 설정할 것이 많음
// ThemeContext.js
import React from 'react';
// Context 생성
const ThemeContext = React.createContext('light');
export default ThemeContext;
// App.js
import React from 'react';
// Context 호출
import ThemeContext from './ThemeContext';
import ChildComponent from './ChildComponent';
function App() {
return (
// Context를 사용할 컴포넌트를 묶고 Provider로 Context 설정
<ThemeContext.Provider value="dark">
<ChildComponent />
</ThemeContext.Provider>
);
}
export default App;
// ChildComponent.js
import React, { useContext } from 'react';
// Context 호출
import ThemeContext from './ThemeContext';
function ChildComponent() {
// Context 호출
const theme = useContext(ThemeContext);
return <p>The theme is {theme}.</p>;
}
export default ChildComponent;
Redux
https://ko.redux.js.org/introduction/getting-started
FLUX 패턴 사용 (하나의 Store 사용, 데이터가 단방향으로 흐르는 패턴)
Store 하나에서 변수를 관리하기 때문에 여러 Provider 불필요
Thunk, saga 등의 Middleware를 통한 비동기 처리
Redux ToolKit 사용으로 간단하게 적용 가능
설정할 것이 많음
FLUX 패턴 적용으로 인한 효율 저하
// actions.js
// 액션 타입 및 생성자 정의
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });
// reducer.js
import { INCREMENT, DECREMENT } from './actions';
const initialState = 0;
// reducer 생성
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
default:
return state;
}
};
export default counterReducer;
// store.js
import { createStore } from 'redux';
import counterReducer from './reducer';
// store 생성
const store = createStore(counterReducer);
export default store;
// Counter.js
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from './actions';
// 컴포넌트에서 호출
const Counter = ({ count, increment, decrement }) => (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
const mapStateToProps = state => ({ count: state });
const mapDispatchToProps = { increment, decrement };
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
Redux를 가장 많이 사용했기 때문에 익숙했다. 그리고 백엔드에 HTTP 요청하기위해 React ToolKit의 Thunk를 사용한 경험이 있다.
1) 함수 호출
2) 백엔드에 HTTP 요청
3) 성공시 Redux State 변경하는 함수 실행
export const putExtk = createAsyncThunk(
'Ext/putExt',
async (obj) => {
const id = obj.id
const res = await axios.put(`http://xxx:3000/${id}`, obj)
return obj;
}
)
.
.
.
.addCase(putExtBlockFixedChk.fulfilled, (state, action) => {
const { id, currentChk } = action.payload;
const item = state.ExtBlockFixed.find(item => item.id === id);
if (item) {
item.isChecked = currentChk;
}
})
Recoil
https://recoiljs.org/ko/docs/introduction/getting-started
필요한 값만 사용하는 느낌
구조가 간단해서 적용이 쉬움
Concurrent mode 지원
일반적으로 렌더링 효율이 좋음
atom이 많아질 수 있음
Middleware 지원 없음
Recoil은 이번에 처음 알게 되었다. 변수 상태를 atom과 selector라는 단위로 관리하기 때문에 필요한 것만 구독하게 되고 불필요한 렌더링을 방지하기 때문에 성능면에서 좋은 장점이 있다.
// atom.js
import { atom } from 'recoil';
// atom 생성
export const textState = atom({
key: 'textState', // unique ID (with respect to other atoms/selectors)
default: '', // default value (aka initial value)
});
// MyComponent.js
import React from 'react';
import { useRecoilState } from 'recoil';
import { textState } from './atom';
function MyComponent() {
// textState를 Recoil 상태를 구독하고 있는 컴포넌트
const [text, setText] = useRecoilState(textState);
const onChange = (event) => {
setText(event.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
<br />
Echo: {text}
</div>
);
}
export default MyComponent;
Zustand
https://github.com/pmndrs/zustand
사용법이 가장 간단하다
Shallow comparison을 통한 효율 개선
함수형, 클래스형 모두 사용 가능
Middleware 지원 없음
Zustand는 상태가 변경될 때 Shallow comparison을 통해 변화를 감지하게 되는데 필요한 컴포넌트만 리렌더링 되도록 하여 성능을 향상할 수 있다.
하지만 Recoil과 마찬가지로 Middleware가 없기 때문에 별도의 HTTP 요청이 필요하다.
// store.js
import create from 'zustand';
// Zustand Store 생성
const useStore = create(set => ({
count: 0,
increase: () => set(state => ({ count: state.count + 1 })),
decrease: () => set(state => ({ count: state.count - 1 })),
}));
export default useStore;
// MyComponent.js
import React from 'react';
import useStore from './store';
function MyComponent() {
// 컴포넌트에서 호출
const { count, increase, decrease } = useStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increase}>Increase</button>
<button onClick={decrease}>Decrease</button>
</div>
);
}
export default MyComponent;
1-3) React Query
Server State - React Query
비즈니스 로직들을 대부분 백엔드에서 관리
서버에서 불러온 값들을 클라이언트가 관리하지 않기
이는 12월 챌린지에서 살짝 다뤘던 주제랑 일치한다. 클라이언트는 서버에서 필요한 데이터만 가져와서 사용해야 클라이언트(프론트 엔드)의 복잡도를 줄일 수 있기 때문이다.
React Query는 서버에서 가져온 데이터를 자동으로 캐싱하고 UI를 랜더링 하기 때문에 프론트에서 데이터를 직접 관리할 필요가 없다.
2-1) 지옥으로 가는 길은 "프론트에서 해드릴게요"로 포장되어 있다
“지옥으로 가는 길은 선의로 포장되어 있다” - 서양 속담
React Query 최적화
Cache된 데이터의 상태
- Fresh
- Stale
- Inactive
Fresh: 최근에 가져온 데이터로, 아직 만료되지 않았다.
Stale: 만료된 데이터로, 다음 렌더링 때 새로 가져올 필요가 있다.
Inactive: 현재 사용되지 않는 데이터로, 메모리를 절약하기 위해 곧 제거될 수 있다.
React Query는 staleTime과 cacheTime 두 가지 시간을 통해 캐시된 데이터를 관리한다.
staleTime은 데이터가 언제 "Stale" 상태가 되는지 결정하고(staleTime이 10분일 경우 데디터를 가져온 후 10분 동안 "Fresh"로 간주)
cacheTime은 "Inactive" 상태의 데이터가 캐시에서 언제 제거되는지 결정한다. (cacheTime이 10분일 경우 데이터를 마지막으로 사용한 후 10분 동안 데이터가 "Inactive" 상태로 남아있으면 캐시에서 제거)
https://fe-developers.kakaoent.com/2023/230720-react-query/
1-4) 렌더링 효율 비교
오늘 알아본 상태관리 툴을 렌더링 효율성면에서 비교하자면 크게 의미 없다고 한다.
굳이 비교자하면 Context가 상태 변경 시 동시에 많은 부분을 리렌더링 하기 때문에 떨어진다고 하는데 이 마저도 상황에 따라 다른 결과가 나오기 때문에 매번 다른 결과가 나온다고 한다.
https://www.youtube.com/watch?v=NwLWX2RNVcw
📝 오늘의 3줄 요약
1. 상태관리를 전역으로 하기 위한 툴엔 알지 못하는 것도 많다.
2. 각각 툴의 장점과 단점을 파악하고 상황에 맞게 사용하자.
3. 신입/주니어 개발자는 다양한 툴의 사용 보단 이유있는 선택을 통해 한 가지 툴을 깊게 사용해 보자.