프론트엔드 데브코스 5기 TIL 56

김영현·2023년 12월 13일
2

TIL

목록 보기
65/129

useImperativeHandle

리액트 빌트인 훅이다.

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/
멘토님이 추천해주셨던 글 보고 더 공부해보자


Redux + TS

타입스크립트와 리덕스를 이용하여 간단한 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가 존재하게되고 다시적자니 귀찮다 ㅋㅋ

re-export

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'

redux-logger

리덕스의 상태변화가 어떻게 일어나는지 추적하는 미들웨어다.
타입을 기본적으로 제공해주지 않는 라이브러리임. 따로 설치해주어야함.
@types/redux-logger

리덕스의 상태변화를 어떻게 추적할 수 있을까?
미리 예측해보자면 다음과 같다.

  • view => action => reducer => store의 구조로 인하여actionreducer로 전달할때 관찰한다면 추적하기 용이할 것이다.
    따라서 리듀서순수해야한다.
//createStore는 사실 인자를 여럿 받는다. 
//createStore(reducer, [preloadedState], [enhancer]). enhancer는 기존의 기능을 향상시키는데 사용된다. 
//리덕스에서는 미들웨어를 추가하는 인핸서를 기본적으로 제공한다. (applyMiddleware)

import logger from 'redux-logger'
...
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(logger)))

리덕스의 각 메서드는 여기서 볼수있다.


이런식으로 값이 나온다. 어떻게 만들었는지 쉽게 유추할수있다.

redux-devtools

이 또한 리덕스 디버깅 툴이다. 타입 포함!
설치 후 크롬 익스텐션까지 설치하면 개발자 도구 탭에서 볼 수 있다.

단순히 콘솔로그로 찍히는 것 보다 훨씬 좋다!
상태를 이전으로 되돌리거나, 앞으로 가거나 할 수 있다.
=> 크롬 디버거와 똑같다. 아주 유용한 기능임.

redux-persist

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-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인 참조형 데이터를 건네준다.

createSlice

위에서 사용했던 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))

useDispatch()

리액트 훅이 나오고나서 dispatch대신 useDispatch()를 사용해야한다.
하지만 구현체를 보면 사실...

    const store = useStore()
    // @ts-ignore
    return store.dispatch

출처는 redux github

함수형이기에 구색을 맞춰준걸까?


redux-thunk

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) => {
			
        },
    }
})

에러 핸들링 방식 rejectWithValue() vs unwrapResult()

에러처리는 createAsyncThunk()함수 내부에서 rejectWithValue함수를 꺼내와 catch문에서 호출해주면 extrareducers내부 rejected로 값이 반환된다.
이때 에러값은 payload로 들어온다. (위에서 사용한 방식)


출처 : https://redux-toolkit.js.org/api/createAsyncThunk

또한 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같은 메서드를 사용하여 결과를 꺼낸 뒤 성공하였더라도 원하는 값이 아니면 에러처리를 할수 있겠다.


느낀점

길어보였는데 막상 하고나니 재밌었다. 또한 노션클론과제때 리덕스를 만드는 경험을 해서 그런지 이해가 정말 잘됐다. 물론 사용하는건 별개의 영역이겠지만...템플릿 자체는 반복해서 사용하면 익숙해 질 것이다.

그리고 특별히 오프라인 모각코를 진행했는데, 집중이 더 잘되는 것 같았다. 커피를 두잔이나 마셔서 그런걸수도? 이래서 카페가서 공부를 하나 싶다.
끝난 뒤엔 팀원분들과 저녁 먹으러 궈궈😎

profile
모르는 것을 모른다고 하기

0개의 댓글