대망의 첫주가 흘러가고 , 어영부영 Best Practice 과제를 마무리하고 둘째주가 되었습니다.
2주차의 첫번째 세션 주제는 과제 피드백과 성능 최적화에 대한 내용으로, 모든 팀에 코드리뷰라던지 피드백이 주어지지 않고 전체적인 피드백을 진행해주셨습니다.
아무래도 인증 & 인가를 구현하고 투두리스트를 만드는 작업은 대부분 비슷하게 흘러가기 때문에 과제 전체적으로 중복되는 코드가 많이 존재했고, 이를 한파트씩 묶어서 피드백을 진행해주셨습니다.
- 코드를 설명하는 주석, 나중에 사용될 수도 있는 코드를 주석처리
- 새롭게 팀에 합류한 개발자 등 멀리 보았을 때 해당 주석이 정말로 필요한 주석인지 모르기 때문에 불필요하게 존재될 수 있는 요소들은 최대한 제거하는쪽이 좋다.
- 해당 코드를 설명하는 코드는 똑같은 코드를 두번 작성하는것과 유사하다.
- 코드가 수정될 때 , 주석도 수정되어야 하기 때문에 중복된 코드를 작성하는것과 같다.
- 함수명은 함수가 무슨 동작을 할 수 있는지 표현하는게 기본이다.
- 해당 함수가 동작하는 기본 이벤트 (예: click, down) 같은 것 보단 확실한 표현으로 작명해준다. (예: toggle)
- 단순히 클릭을 다루는 함수더라도 해당 클릭이 어떠한 행동을 하는지 명확하게 표현해야한다.
- 리액트 공식 문서에서는 불필요한 state 의 생성을 지양하라고 적혀있다.
- 리액트의 기본 동작 원리를 참고하여 불필요한 state를 줄여 코드의 가독성을 높이고 성능까지 챙겨갈 수 있다.
- 예 : 인증에 필요한 로직을 구현시에 사용되는 검증에대한 boolean, 버튼의 disabled 를 표현하는 boolean 값들에 대한 불필요한 state 제거 (리액트는 state 값이 변경되거나 props의 상태가 변경되면 리렌더링을 하는 특성이 있다.)
- 함수에 한번에 두가지 이상의 일을 처리하도록 구현하면 안좋다. (의존하게됨)
- 사전과제 중 회원가입과 로그인의 핵심 로직이 submit을 제외하고는 거의 비슷해서 해당 부분을 하나의 컴포넌트로 통합해서 개발하신 팀들이 좀 있었다. (본인 포함)
- 개인 의견으로는 그렇게 개발하는 것은 높은 결합도를 가지게 되며 , 코드의 복잡도나 유지보수측면에서 안좋은 결과를 가져오게 될 수 있다.
- useMemo 와 useCallback 의 동작 원리를 이해하고 , 비용이 더 낮은것을 우선으로 구현해라.
- useMemo 와 useCallback 은 마찬가지로 memoization 의 원리를 이용해 구현된 훅인데 , 보통 계산 비용이 큰 값을 재사용 할 때 사용한다.
- 우리팀 같은 경우에는 useMemo 와 useCallback 을 억지로 사용할 필요가 없다고 판단하여 사용하지 않았다. 재사용 될 컴포넌트는 존재하지 않았고, 자바스크립트는 기본적으로 변수에 저장된 값으로 동일한지 판단하는게 아닌 변수가 가리키는 메모리 공간으로 판단하기 때문에 최적화를 시키는데 들어가는 비용이 단순히 주어진대로 구현하는것보다 크다고 생각했다.
- 함수의 경우에도 재생성을 방지하도록 구현할 정도로 소모비용이 큰 함수는 존재하지 않았다.
- 가장 핵심이 되는 코드를 상단에 배치해라.
- interface 나 styledcomponent 를 사용하여 스타일링 할 때 , 해당 코드들을 상단에 배치하지 말고 핵심 코드를 상단에 배치하자.
저는 항상 무언가를 복습할 때 굉장한 실력향상이 되었다고 생각했었기 때문에, 그대로 똑같이 피드백을 참고하여 팀과제를 진행하면서 괜찮았던 부분을 반영시켜 투두리스트 프로젝트를 개인으로 다시 진행하였습니다.
//기존의 작성 순서
const StyledButton = styled(Button)`
display: flex;
height: 50px;
gap: 10px;
color: black;
`;
interface MenuProps {
label : string;
icon : ReactNode;
link : string;
}
const HeaderMenu = () => {
const navigate = useNavigate();
const tokenState = useTokenState();
const isAuth = tokenState.accessToken !== null;
...
// 이후의 작성순서
const TodoList = ({ todos, getTodos, error, loading }: ITodoListProps) => {
return (
<S.Container>
{!!todos &&
!loading &&
todos.map((todo, index) => {
return <TodoItem key={index} todo={todo} getTodos={getTodos} />;
})}
{error && <div>에러가 발생했습니다.</div>}
{loading && <Loading />}
</S.Container>
);
};
interface ITodoListProps {
todos: ITodo[];
getTodos: (data?: any) => Promise<void>;
error?: any;
loading?: boolean;
}
export default TodoList;
// 기존의 훅
export function useFormControl(options: {
regex: RegExp;
initialValue?: string;
}): [React.ChangeEventHandler<HTMLInputElement>, string,React.Dispatch<React.SetStateAction<string>>, boolean, React.Dispatch<React.SetStateAction<boolean>>,] {
const { regex } = options || {};
const [value, setValue] = useState(options.initialValue || "");
const [validation, setValidation] = useState(false);
...
// 이후의 훅
const useInput = <T,>({ initialValue, regex, refObject }: UseInputProps<T>) => {
const [value, setValue] = useState<T>(initialValue);
const isValidate = regex && typeof value === 'string' ? regex.test(value) : true;
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value as unknown as T);
};
...
// 기존 코드
...
const authHandler = (mode: string, user: IUser) => {
setIsLoading(true);
// 로그인
if (mode === 'signIn') {
signIn(user)
.then((res) => {
if (res.access_token) {
localStorage.setItem('token', res.access_token);
navigate('/todo');
setIsLoading(false);
alert('로그인 되었습니다.');
}
})
.catch((e) => {
if (e.response.status === 404) {
setUnAuthorizedError();
} else if (e.response.status === 401) {
setUnAuthorizedError();
}
setIsLoading(false);
});
} else {
signUp(user)
.then((res) => {
if (res) {
navigate('/signin');
setIsLoading(false);
}
})
.catch((e) => {
if (e.response.status === 400) {
setExistUserError();
}
setIsLoading(false);
});
}
};
...
// 이후의 코드
...
interface IAuthProps {
request: (data?: any) => Promise<void>;
error?: any;
loading?: boolean;
isSuccess?: boolean;
title: string;
testId: string;
}
...
확실히 개인으로 프로젝트를 진행했을 때 부족했던 부분을 bestpractice 과제를 진행하여 고치다보니 훨씬 전체적으로 완성도 높은 프로젝트를 다시 진행 할 수 있었습니다.
제가 소속되지 않았던 팀들의 과제도 참고하다보니 좋은 레퍼런스가 될만한 것들이 많았습니다. 같은 과제를 여러팀이 수행하다보니까 참고할 부분들도 많아서 실력향상에 도움이 된 것 같습니다.
기존에 단순 material ui 라이브러리를 사용하여 구현했던 것과 달리 , 이번 팀과제때 구현했던 common 컴포넌트를 다시 새롭게 구현해서 제 디자인을 입혀 구현해보았는데, ui 적인것 외에 기능적인 부분을 고민하다보니 나중에 협업할 때 해당 과정이 큰 도움이 될 것 같다는 생각이 들었습니다.
저는 평소에 도대체 useMemo 와 useCallback 은 어느 상황에 사용해야할까..? 에 대한 궁금증이 굉장히 많았습니다. 어떻게 보면 프리온보딩을 신청한 이유도 리액트의 hook들의 활용 용도를 다시 한번 짚어볼 수 있겠다는 생각에 참여하기도 했기 때문에 굉장히 열심히 수강하였습니다.
리액트에서는 리랜더링 될 떄 , 기본적으로 기존의 UI를 재사용 할 수 있는지 확인 후 , 컴포넌트 함수를 호출하여 이전의 가상 돔과 새로 생성된 가상 돔을 비교하여 변경된 부분을 반영합니다.
이를 저는 state 가 변경되면 리랜더링 된다고 간단하게 생각하고 있었는데요,
리액트에서의 성능 최적화는 보통 이 리랜더링을 얼마나 줄이느냐가 가장 중요하게 여겨지는것 같았습니다.
useCallback 이나 memo 를 무분별하게 남발하여 작성되는 경우도 많고 (물론 저 포함) 의존성 배열에 적절한 값을 입력하지 않아서 발생되는 성능 저하도 있기 때문에,
대부분의 경우에는 불필요한 최적화 보다는 코드의 퀄리티를 향상시키는 쪽이 더 좋을것이라는 내용인것 같았습니다.
실제로도 신입 개발자가 주니어로 취업하기 이전에 겪을 수 있는 것들 중 성능저하를 최적화 시킨다던지, 서버 장애를 복구한다던지.. 그런 경우는 직접 서비스를 운영하거나 규모가 작지 않은 프로젝트를 진행하지 않는 이상 겪기 힘들기 때문에,
어떠한 경우에서 사용해야하는지 보다는 어떻게 값을 비교해서 값 재생성을 방지하거나 컴포넌트 리렌더링을 방지할 수 있는지 정도로만 알게되었습니다.
react memo 같은 경우에는 단순히 이전의 props 와 새로운 props 를 얕은 비교를 통해 리렌더링 할지 말지 여부를 결정하는 것이었습니다.
예를 들어서 실시간으로 어떠한 게시글의 조회수를 서버에서 불러와 반영하는 컴포넌트를 구현한다 했을 때 , 해당 컴포넌트가 동일한 고유 식별자를 갖지만 조회수가 변경되지 않을 때에도 재랜더링이 되는 상황 같을 때 사용이 가능할 것 같았습니다.
기존에 진행했던 과제의 경우엔 투두리스트로 memo를 사용할 일이 거의 없었는데 , 어거지로 투두의 내용이 같으면 ui 를 재사용하도록 구현해버리면, 삭제나 수정을 해버렸을 때 상단으로 올라온 재사용된 투두가 id 같은 고유 식별자 또한 재사용된 투두를 참조해버리기 때문에 사용할 이유가 없었습니다.
React.memo 를 사용하여 컴포넌트를 재사용 하도록 했을 때 , 리랜더링이 일어나지 않습니다. 하지만 브라우저는 최상단의 투두를 삭제하는것이 아닌 브라우저상에서 없어지는 값(최하단의 투두) 을 지워버립니다. 따라서 같은 삭제했던 투두의 아이디를 참조하게 됩니다.
무엇이든 같겠지만, 필요하지 않는 상황에서의 사용은 오히려 성능 저하를 일으킬 수 있으니 해당 부분은 조심해서 사용해야겠구나를 느끼게 되었습니다. 😒
2주차 두번째 세션에서는 관심사의 분리를 주제로 강의가 진행되었습니다. 관심사의 분리는 백엔드를 공부했던 저는 굉장히 친근했던 주제였는데요, 스프링부트를 공부할 때 관점 지향 프로그래밍에서 자주 다뤘던 주제였습니다.
단순히 공통 함수를 밖으로 빼버려서 여러곳에서 중복으로 관리될 수 있는 것들을 하나로 관리함으로써 유지보수성을 높이는게 가장 큰 목표라고 생각했습니다.
해당 세션의 메인 주제는 UI와 기능을 분리하여 UI를 다루는 컴포넌트에서는 UI를 담당하는 로직만 남아있고, 데이터 관리, http 통신에 관한 기능들은 따로 커스텀 훅을 구현하는 등의 방법으로 각각 컴포넌트에서 필요한 기능들만 남아있도록 유지보수성을 높이는 것이었습니다.
이때 가장 충격적이었던 것은, 스프링부트를 공부할 때 자주 등장했던 의존성 주입, 계층구조를 도입하여 리액트에서 유지보수성을 높이는 방법을 알려주셨습니다.
횡단은 단순히 횡단보도에 사용되는 단어와 같은 뜻인데요, 횡단 관심사는 말 그대로 관심사 사이를 횡단하는 것으로 더 쉽게 말해서 공통으로 실행되는 행동들이라고 생각하면 될 것 같습니다.
예를 들어서 트랜잭션, 인증 & 인가, 로깅 등이 있는데 강사님께서는 해당 부분을 fetch 를 사용한 http 통신을 클래스를 사용하여 구현을 하셨습니다.
기존에 제가 axios interceptor 를 사용하여 토큰의 유무를 판단하여 헤더에 넣어주도록 구현한 부분 또한 횡단 관심사 처리를 위한 부분이었어서 나름 적절하게 코드를 작성했구나 하는 안도감이 들었습니다. 🥴
어떻게 보면 비슷한게, 결국 axios.create 를 사용하면 axios instance를 반환하기 때문에..
여기서 더 충격이었던건 강사님께서는 단순 함수로 api들을 관리하시지 않으시고 Service 클래스를 선언하셔서 관리했습니다.
생성자에 따로 미리 선언한 HttpClient (fetch 로직들을 구현해둔)를 의존성으로 주입하여 해당 서비스 클래스에서 필요한 로직들을 주입받은 HttpClient 를 통해서 처리하도록 구현하는 것이었는데
이부분에 대해서는 이렇게 구현해도 괜찮나? 하는 고민이 많았습니다.
이부분은 확실히 신선하게 다가온 방법이라 굉장히 충격적이었고, 더 나은 구현법이 있는지 이번 기회를 통해서 언어에 갇히지 않고 오픈마인드로 고민해봐야겠다는 생각이 들었습니다.
2주차는 다행히 팀과제가 없어서 복습과 함께 기본 동작 원리에 대해서 알아갈 수 있는 시간을 가졌습니다. 가장 많이 얻어갔던건 리랜더링의 원리와 SOLID원칙을 비슷하게나마 리액트에도 적용할 수 있다는 것이었고, 어쩌면 자바에서 구현했던 것들도 참신하게 리액트에 적용할 수 있지 않을까 하는 기대감도 생긴것 같습니다.
횡단 관심사 부분을 다시 한번 복습하여 강의에서 진행했던 클래스로 구현하는 것들을 다시 적용시켜봐야겠습니다 😎