ref
에 메서드를 할당할수 있음.
//App.js
const inputRef = useRef();
return(
<input ref={inputRef} />
<button onClick={() => inputRef.current.clear()} />
)
//Input.js
//내부에서 ref를 한번 더 선언하여 사용한다.
const Input = forwardRef((_, ref)) => {
const inputRef = useRef();
//프롭스로 받아온 `ref`에 메서드를 추가한다.
useImperativeHandle(ref, () => ({
clear:()=>{
inputRef.current.value = ''
}
}))
return(
<input ref={inputRef} />
)
}
부모가 가진 ref
에 자식의ref
값을 지우는 메서드를 추가했다.
높은 응집도
공통 폐쇄 원칙이 과하면 재사용성이 낮아지고 재사용 원칙이 과하면 컴포넌트의 수가 많아짐.
큰 프로젝트라면 컴포넌트의 수가 많아지는게 낫고 아니라면 그냥 뭉탱이로 하는게 나을거같다.
결합도
https://fe-developers.kakaoent.com/2023/230330-frontend-solid/
멘토님이 추천해주셨던 글 보고 더 공부해보자
타입스크립트와 리덕스를 이용하여 간단한 Todo를 만들어보자.
//type.ts
// 리듀서, 액션, 상태의 타입
interface Task {
id: string,
content: string,
isCompleted: boolean
}
const ADD_TASK = 'ADD_TASK'
const UPDATE_TASK = 'UPDATE_TASK'
const REMOVE_TASK = 'REMOVE_TASK'
type ActionType = ADD_TASK | UPDATE_TASK | REMOVE_TASK
type Action = { type: ActionType, payload: Task }
//actions.ts
const addTask = (content: string):Action => {
return {
type:ADD_TASK
paylod:{
id:v4(),
content,
isCompleted:false
}
}
};
const updateTask = (id: string, content:string, isCompleted:boolean):Action => {
return {
type:UPDATE_TASK
paylod:{
id,
content,
isCompleted
}
}
};
//함수의 타입을 Action으로 주면, payload가 필요없는 프로퍼티를 갖게됨...
const removeTask = (id: string):Action => {
return {
type:REMOVE_TASK
paylod:{
id,
content:'',
isCompleted:false
}
}
};
//reducer.ts
const tasks = (state: Task[] = [], action: Action) => {
switch(action.type){
case: ADD_TASK:{
const newTask = action.payload;
return [...state, newTask];
}
case: UPDATE_TASK:{
const updatedTask = action.payload;
return state.map((oldTask) => oldTask.id === updatedTask.id ? updatedTask : oldTask)
}
case: REMOVE_TASK:{
const { id } = action.payload;
return state.filter((oldTask) => oldTask.id !== id)
}
default:{
return state
}
}
}
//index.ts (리듀서가 여럿 존재할 수 있으므로 합쳐주어야한다)
const rootReducer = combineReducers({tasks});
const store = createStore(rootReducer) //합쳐진 reducer로 스토어 생성
//combineReducer로 rootState가 생성되고, 전달한 reducer의 이름으로 프로퍼티가 생성된다. ex) rootState.tasks
//이때 rootState의 타입은 리덕스가 제공한 기본타입이어서 명시적으로 지정해주어야함.
//RetunType<Type> 이라는 유틸리티 타입은 'Type`의 반환타입으로 구성된 타입을 반환한다.
//이는 combineReducers 역시 각 리듀서를 돌며 default값으로 반환된 객체를 리턴함을 의미한다.
type RootState = ReturnType<typeof rootReducer>
//App.ts Provider로 store를 사용할 컴포넌트를 감싸준다. 전역으로 사용할테니 App을 감싼다.
<Provider store={store}>
<App/>
</Provider>
removeTask
함수 타입을 어떻게 지정할지 고민이다.
Action
이라는 타입을 사용하게되면 필요없는 payload
의 프로퍼티content, isCompleted
가 존재하게되고 다시적자니 귀찮다 ㅋㅋ
export * from "module"
타입스크립트 공식문서 설명
위처럼 re-export를 통하여 흩어져있는 모듈을 한군데의 index.ts
에 모아서 export
하면 파일을 쉽게 관리할 수 있다.
//test/A.ts
export const A...
//test/B.ts
export const B...
//test/index.ts
export * from './A';
export * from './B';
//다른데서 사용할 때
import { A, B } from '../test'
리덕스의 상태변화가 어떻게 일어나는지 추적하는 미들웨어다.
타입을 기본적으로 제공해주지 않는 라이브러리임. 따로 설치해주어야함.
@types/redux-logger
리덕스의 상태변화를 어떻게 추적할 수 있을까?
미리 예측해보자면 다음과 같다.
//createStore는 사실 인자를 여럿 받는다.
//createStore(reducer, [preloadedState], [enhancer]). enhancer는 기존의 기능을 향상시키는데 사용된다.
//리덕스에서는 미들웨어를 추가하는 인핸서를 기본적으로 제공한다. (applyMiddleware)
import logger from 'redux-logger'
...
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(logger)))
리덕스의 각 메서드는 여기서 볼수있다.
이런식으로 값이 나온다. 어떻게 만들었는지 쉽게 유추할수있다.
이 또한 리덕스 디버깅 툴이다. 타입 포함!
설치 후 크롬 익스텐션까지 설치하면 개발자 도구 탭에서 볼 수 있다.
단순히 콘솔로그로 찍히는 것 보다 훨씬 좋다!
상태를 이전으로 되돌리거나, 앞으로 가거나 할 수 있다.
=> 크롬 디버거와 똑같다. 아주 유용한 기능임.
persist : 지속하다.
상태를 스토리지(local, session)에 저장하게 도와준다. 업데이트된지 2년이 넘어서...손을 놓은건가?
//index.ts
const persistConfig = {
key: 'root', //스토리지 키 이름
storage: 'session', // local vs session 두가지 사용가능
whitelist: ['tasks'], //어떤 상태를 저장할건지
}
const combinedReducer = combineReducers({tasks});
const rootReducer = persistReducer(persistConfig, cominedReudcer)
const store = createStore(rootReducer, ...미들웨어)
//App.ts
<Provider store={store}>
<PersistGate persistor={persistor}>
<App/>
</PersistGate>
</Provider>
계층을 한번 더 감싸서사용한다. 이때 주의할 점은 redux의 Provider내부에 있어야 함.
Redux-Tool-Kit(이하 RTK)는 무려 리덕스에서 권장하는 방법 이다
리덕스를 써본 사람들은 알겠지만, 보일러 플레이트가 꽤 있다. 반복되는 로직을 작성하는데 지치기도 하고 비동기를 다룰땐 redux-thunk같은 별도의 미들웨어를 사용해야하고 Promise의 3가지 상태를 처리할때도 꽤나 귀찮다.
이를 해결하려고 나온게 RTK니 말 다했다. 그리고 공식 기술이니까 안정성도 입증되었겠지?
//스토어 생성, rootState 타입
const store = configureStore({
reducer: rootReducer, // 리듀서도 명시적으로 설정
middleware: [logger, ...], // 미들웨어를 명시적으로 설정
devTools: true // 개발자 도구가 기본적으로 내장되어있다.
})
type RootState = ReturnType<typeof store.getState>
//액션은 createAction(actionType, parameter)를 이용한다.
const addTask = createAction('tasks/add', content:string) => {
return{
payload:{
id:v4(),
content,
isCompleted:false
}
}
}
//리듀서 생성또한 createReducer()함수를 이용한다.
const tasks = createReducer([] as Task[], {
//PayloadAction은 RTK에서 기본적으로 제공하는 제네릭이다.
[addTask.type]: (state: Task[], action: PayloadAction<Task>) => {
//RTK는 내부적으로 'immer'라이브러리를 사용하기에 스프레드 연산자를 사용하지 않아도 '거의 깊은 복제본'으로 제공된다.
state.push(action.payload)
}
})
객체 전부를 깊은복제하는게 아니라 변경된 데이터만 바꾸어서 나머지 참조는 그대로 건네준다. 이때 Readonly인 참조형 데이터를 건네준다.
위에서 사용했던 createAction, createReducer
를 한군데 모아 만들 수 있는 함수가 존재한다. 리덕스의 Ducks Pattern과 유사하지만, 아예 하나의 함수로 만들었단게 정말 크다! 귀찮음이 대폭 줄고 파일 여기저기를 봐야하는 비용이 줄어들 것이다.
const tasks = createSlice({
name: 'tasks',
initialState: [] as Task[],
redcuers: {
add: {
reducer: (state: Task[], action: PayloadAction<Task>) => {
state.push(action.payload)
},
//액션함수는 prepare라는 메서드로 정의한다.
prepare: (content: string) => ({
payload:{
id:v4(),
content,
isCompleted:false
}
})
},
update: {...},
remove: {...}
}
})
//index.ts
//slice는 action,reducer 두개를 갖고있기에 리듀서를 넘겨줄때 slicename.reducer 이렇게 넘겨주어야한다.
const combinedReducer = combineReducers({tasks: tasks.reducer})
//사용할 때는 아래와 같다.
//redux
dispatch(addTask(task));
//RTK
dispatch(tasks.actions.add(task))
리액트 훅이 나오고나서 dispatch
대신 useDispatch()
를 사용해야한다.
하지만 구현체를 보면 사실...
const store = useStore()
// @ts-ignore
return store.dispatch
출처는 redux github
함수형이기에 구색을 맞춰준걸까?
thunk란 일부 지연된 작업을 수행하는 코드조각 모음이다.
https://en.wikipedia.org/wiki/Thunk
기본 redux에서 비동기 통신을 제어할때 사용하는 미들웨어다. 표준임.
리듀서는 순수해야 한다. 그렇기에 async
같은 키워드를 사용할 수 없다.
이번 파트에서는 기본 redux가 아닌 RTK에서 thunk를 사용하는 방법을 알아보겠다.
const fetchAllPost = createAsyncThunk('posts/fetchAllPost', async ({{ rejectWithValue }}) => {
try {
const { data } = await axios.get('...');
return data;
} catch(error) {
rejectWithValue(error.response.data);
}
})
const posts = createSlice({
name: 'posts',
initialState: {
data: [],
isLoading: false,
},
extraReducers: {
//값 반환 대기중
[fetchAllPost.pending.type]: (state) => {
state.data = [];
state.isLoading = true;
},
//값 반환 완료
[fetchAllPost.fulfilled.type]: (state, action) => {
state.data = action.payload
state.isLoading = false;
},
//프라미스 에러처리
[fetchAllPost.rejected.type]: (state, action) => {
},
}
})
에러처리는 createAsyncThunk()
함수 내부에서 rejectWithValue
함수를 꺼내와 catch
문에서 호출해주면 extrareducers
내부 rejected
로 값이 반환된다.
이때 에러값은 payload
로 들어온다. (위에서 사용한 방식)
또한 createAsyncThunk
내부에서 처리할수도 있다.
const fetchUser = createAsyncThunk('user/fetchUser', async (userId, { unwrapResult }) => {
try {
const response = await api.getUser(userId);
return response.data; // 성공한 경우에는 반환된 데이터가 payload로 설정됨
} catch (error) {
throw error; // 실패한 경우에는 에러를 다시 던져서 rejectWithValue로 이동하게 함
}
});
아직까지 무슨 차이인지 자세히 모르겠다. 다만 조금 더 세세하게 에러를 다루고싶다면, unwrapResult
같은 메서드를 사용하여 결과를 꺼낸 뒤 성공하였더라도 원하는 값이 아니면 에러처리를 할수 있겠다.
길어보였는데 막상 하고나니 재밌었다. 또한 노션클론과제때 리덕스를 만드는 경험을 해서 그런지 이해가 정말 잘됐다. 물론 사용하는건 별개의 영역이겠지만...템플릿 자체는 반복해서 사용하면 익숙해 질 것이다.
그리고 특별히 오프라인 모각코를 진행했는데, 집중이 더 잘되는 것 같았다. 커피를 두잔이나 마셔서 그런걸수도? 이래서 카페가서 공부를 하나 싶다.
끝난 뒤엔 팀원분들과 저녁 먹으러 궈궈😎