-
1/3 프론트엔드 온보딩 인턴십 내용 정리.원티드 프리온보딩 2023. 1. 4. 13:56
전체 조 사전과제 피드백 내용
1.
axiosError와 try catch 구문을 사용할 때, 그 구조를 명확하게 설계해야 함. 계획없이 무지성으로 try catch를 사용하면 에러 이벤트의 전달이 감지되어 예외처리를 실행해야하는 구문에서 처리가 제대로 되지 않거나 그 구조가 복잡해질 수 있기 때문에 그 에러의 처리와 위임의 구조를 잘 설계해야 할 필요가 있다.
try { const { data } = fetchData(); } catch (e) { throw e; // or throw new Error({ e.status, e.message }); // or throw new Error("Axios Error"); }
참고로 catch 문에서 발생하는 에러객체인 'e'를 그대로 throw 하거나 새로운 에러객체를 만들어 'e'의 몇가지 속성만 부분적으로 전달할 수 있다. 전자의 방식은 잘 사용하지 않았는데, 저런 방식도 사용할 수 있구나 느꼈음.
2.
파일을 열었을 때 상대적으로 덜 중요한 내용인 타입이나 스타일드 컴포넌트 변수들이 나오게 될 경우, 파일의 가독성이 낮아질 수 있음.
애초에 다른 파일로 분리를 하거나 하단에 배치하여 가장 중요한 내용인 컴포넌트가 바로 보이도록 하는 것이 좋음.
3.
css 스타일 속성을 작성할 때, 그 작성 순서에 일관성을 부여해주는 것이 좋다. 그 예시로 width, height, margin, padding, display 등의 순서로 스타일 속성을 작성한다면 다른 곳에서도 그 순서를 지켜 작성해주는 것이 가독성에 좋다는 의미.
4.
태그 선택자는 의도한 그 태그들만 선택될 것이라는 확신이 있는 경우가 아니라면 클래스나 id 등을 사용한 선택자를 사용한다. 태그 선택자는 지양하는 것이 좋음.
5.
리액트를 사용할 때는 컴포넌트 내부에 최소한 개수의 state를 지정해 주어야 함. 각 state들은 독립적으로 이루어져야 하고(서로 의존성이 없어야 함.) 한 state가 바뀌어 영향을 받아 연쇄적으로 바뀌게 되는 데이터들은 그때 마다 계산되도록 만드는 것이 효율적임. 예를 들어 To-Do 배열을 state로 다루게 될 때, To-Do 배열의 길이를 또 state로써 관리할 필요가 전혀 없다는 의미.
state의 3대 원칙.
- 부모로부터 props를 통해 전달되는 것은 state가 아니다.
- 시간이 지나도, 이벤트가 발생해도 변하지 않으면 state가 아니다.
- 다른 state와 props를 사용해 얻을 수 있는 데이터이면 state가 아니다.
즉, 한 컴포넌트의state를 지정할 때는 이러한 중복배제원칙을 떠올립시다!
수업 내용
1. 기존 DOM api를 활용한 개발과 SPA 개발방식의 근본적인 차이는 무엇인가?
명령형 개발) 바닐라 자바스크립트를 사용하여 DOM에 접근해 화면을 바꾸는 것.
선언적 개발) UI(컴포넌트)를 개발해두고 state의 값을 조작해 그 값에 따른 UI들이 보여지게 하는 것.
이 두 방식으로 차이가 생성된다고 볼 수 있다.
2. React.memo는 HOC(Higher Order Component), 즉 고차 컴포넌트이다.
고차 컴포넌트는 고차 함수와 비슷한 의미인데, 컴포넌트를 인자로 받거나 컴포넌트를 반환값으로 갖는 컴포넌트라는 의미를 가진다.
즉, 함수형 컴포넌트에 React.memo를 사용하게되면 이 React.memo는 고차 컴포넌트이자 고차 함수가 되는 것이다.
이것의 역할은 인자로 받은 컴포넌트의 props 상태를 기억해, 해당 컴포넌트가 재렌더링되는 시점의 props값과 이전의 props값을 얕은 비교(shallow compare)하여 그 props들의 일치 여부로 재렌더링 여부를 심사하는 것이다. 만약 이 props들이 object, array 등의 참조타입일 경우 깊은 비교가 필요할 수 있는데, 이 경우 React.memo의 두 번째 인자로 깊은 비교를 수행할 수 있는 함수를 넣어준다.
function MyComponent(props) { /* render using props */ } function areEqual(prevProps, nextProps) { return (prevProps.obj.value === nextProps.obj.value); } export default React.memo(MyComponent, areEqual);
위의 코드처럼 props 내부 속성에서 원시값을 비교하여 해당 컴포넌트의 재렌더링 여부를 반환해 최적화가 가능하다. 콜백형식으로 또한 함수를 넣어줄 수 있으며 prevProps와 nextProps에는 자동으로 컴포넌트의 이전 props값과 재렌더링 이후의 props값이 들어가 깊은 비교를 수행할 수 있다.
3. useMemo 훅이란?
// useMemo(callbackFunction, deps] const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
React.memo와 useMemo 모두 근본적으로 인자로 들어오는 대상에 대한 내용을 기억한다는 것에는 기능이 동일하다.
memo라는 것 자체가 memoization, 기록이라는 뜻을 가지고 있기 때문이다. 다만 React.memo는 그 대상이 컴포넌트이고 useMemo는 컴포넌트 내부에서만 사용될 수 있는 훅으로서 그 대상은 콜백함수와 의존성 배열이 된다.
useMemo훅 함수가 반환하는 값은 컴포넌트가 재렌더링되더라도 기억되기때문에 다시 계산될 필요가 없다. 즉, 재사용이 가능하다.
그러나 의존성 배열 내부의 요소들이 교체된다면 재렌더링 시 useMemo의 콜백함수는 다시 실행되어 새로운 값을 계산 및 기억한다.
이 부분은 예전에 가고싶던 회사 면접 질문에서 제대로 대답하지 못한 부분이기 때문에 기억에 잘 남을것 같다.
4. useCallback 훅이란?
const memorizedFunction = useMemo(() => () => console.log("Hello World"), []); const memorizedFunction = useCallback(() => console.log("Hello World"), []);
만약 useMemo로 함수를 기억하고자한다면, useMemo의 인자로 받는 콜백함수가 다시 함수를 리턴해주는 형식으로 작성이 된다.
이런 작성방식은 매우 번거롭고 가독성에 좋지 않기때문에 함수만을 기억할 수 있는 용도로 새로운 훅을 만든 것이 바로 useCallback이다.
위 코드에서 확인할 수 있듯이, 인자로 넣어지는 콜백함수 자체가 기억될 내용이기 때문에 useMemo보다 좀 더 편리하게 함수를 기억할 수 있다.
5. 위의 최적화 훅들은 언제 사용해야하는가?
항상 무조건 최적화를 추구하는게 올바른 것은 아니다. 최적화도 하나의 비용이며 최적화 심사를 수행하는 것도 많은 양의 연산이 발생할 수 있기 때문!
또한, 최적화 심사 코드를 작성하는 개발자의 노력 및 그 코드의 양 등.. 이러한 요소를 무시하고 언제나 최적화를 수행하는 것은 옳지 않다.
따라서, 우리는 밑의 질문을 심사숙고해야 한다.
"
새로운 값을 만드는 것과 어딘가에 이전의 값을 저장해두고 메모이제이션 함수를 호출하고 의존성을 비교해서 가져올지 말지 여부를 판단하는 것 중 어떤 것이 비용이 더 적게 들까?
"
우리는 위의 질문을 곱씹어볼 필요가 있다.
만약 useMemo, useCallback에 든 의존성 배열의 요소가 많아 이것들의 변화되었는지 여부를 일일히 검사하기 어려운 경우, React.memo에서 props의 변화 여부를 깊은 비교로 검사하는 코드가 상당히 복잡하여 컴포넌트를 새로 재렌더링하는 것보다 오히려 더 많은 양의 연산이 발생하는 경우 등, 이러한 경우들은 최적화를 수행해야할 것인지 충분한 토의와 숙고가 필요할 것이다.
6. 그렇다면 우리가 최적화 훅들을 꼭 사용해야 할 때는 무엇일까?
1) 새로운 값을 만드는 연산이 복잡할 경우
컴포넌트가 재렌더링될 때마다 메모리를 많이 차지하는 변수를 새로 할당해야하거나 많은 양의 연산을 처리해야하는 경우에는memoization을 수행하는 것이 효율적일 것이다.
2) 함수 컴포넌트의 이전 호출과, 다음 호출 간 사용하는 값의 동일성을 보장하고 싶을 경우
함수 컴포넌트의 props 객체는 재렌더링될 때마다 재할당되어 새로운 주소를 가진 객체로서 교체된다. 하지만 React.memo를 사용하면 이 props객체를 기억하며 이 객체가 가진 속성들만 얕은 비교로서 검사를 수행한다.
7. 의존성 배열에는 어떤 요소가 담겨야 하는가?
useMemo, useCallback, useEffect 등의 훅들의 의존성 배열에 setState 같은 요소들을 넣어줄 때가 있다.
이 setState 함수들이 이 훅들의 콜백함수 내부에서 사용된다면 명시적인 코드를 위해 의존성 배열에 넣어주는 것이 효과적이다.
다만, 수업 도중에 어떤 학생분이 '이 의존성 배열에 담긴 setState 함수들이 컴포넌트가 재렌더링될 때마다 새롭게 선언되어 훅들이 다시 실행되도록 유도하는 것 아닌가?' 라는 좋은 질문을 하셨다.
그 답은 setState, 또는 useNavigate가 반환하는 navigate 등의 리액트 동작에 근본적인 역할을 수행하는 훅의 반환값들은 내부적으로 useMemo 처리가 되어 컴포넌트가 재렌더링되어도 그 객체가 재선언되지않고 여전히 이전의 객체 주소를 기억한다고 강사님이 답변해 주셨다.
따라서, 의존성 배열에 추가하더라도 아무 문제가 없다는 것이다.
아주 좋은 질문이어서 수업 중 졸렸는데, 잠이 확 깼음...
8. 그렇다면 의존성 배열이란 정확하게 무엇인가?
우선, '의존하고 있다'라는 말이 어색할 수 있다. 그냥 이 훅 내부에서 사용하는 외부의 값, 함수 등 요소들을 말하는 것이라고 이해하자.
우리의 목표는 이 의존성 배열을 잘 설정해서 훅의 콜백함수가 배열의 요소가 변화되었을 때, 우리의 의도대로 실행될 수 있도록 하는 것이며,
필요없는 요소들을 의존성 배열에서 배재하여 불필요한 훅의 재실행을 방지하는 것이 훅을 잘 사용하는 방법이자 의존성 배열을 잘 설정하는 방법이다.
좋은 의존성 배열을 설정하는 원칙은 아래와 같다.
1) “모든 의존성을 빼먹지 말고 의존성 배열에 명시해라”
2) “가능하다면 의존성을 적게 만들어라”
모든 의존성을 명시하면서 의존성을 적게 만들라니.. 무슨 소리인지 이해하기 어려울 수 있다.
이 원칙은 필요하지 않거나 생략될 수 있는 의존성은 모두 제거하고 꼭 필요한, essential한 요소만 의존성 배열에 남기라는 뜻이다.
// bad case function App() { const [count, setCount] = useState(0); useEffect(() => { const intervalID = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(intervalID); }, [count, setCount]); return ( <div> <h1>count:{count}</h1> </div> ); } // good case function App() { const [count, setCount] = useState(0); useEffect(() => { const intervalID = setInterval(() => { setCount(prevCount => prevCount + 1); }, 1000); return () => clearInterval(intervalID); }, [setCount]); return ( <div> <h1>count:{count}</h1> </div> ); }
위의 케이스에서 볼 수 있듯, bad case에서의 useEffect가 가진 의존성 배열에는 count, setCount가 있다.
이런 경우는 useEffect의 콜백함수 내부에서 count값에 접근하고 있으니 의존성에 count를 추가해 준 경우일 것이다.
그러나, count가 interval이 설정된 1초 간격으로 증가하면서 다시 useEffect의 콜백함수가 실행되고 또다른 interval이 생성되어 이 컴포넌트는 무한루프에 빠지게 된다.
그러나 아래의 good case에는 setCount만 의존성 배열에 추가되어있으며 필요한 state인 count값은 setState의 콜백함수를 사용하여 처리를 했다.
즉, 같은 로직을 수행함에도 불구하고 의존성을 필요한 요소로만 최소화한다면 의도하지 않은 버그도 방지할 수 있으며, 필요없는 의존성 요소를 제거할 수 있다는 의미!
이렇게 불필요한 의존성을 줄여간다면 코드 파악도 용이하고 컴포넌트 재렌더링 시 의존성 배열을 비교하는 연산을 줄여 성능에도 도움이 된다.
또다른 케이스를 보자.
function Component(){ const [count, setCount] = useState(0); const increaseCount = () => { setCount(prev => prev + 1); } useEffect(increaseCount, [increaseCount, setCount]]; }
이 경우 increaseCount 라는 함수는 memoization되지 않았기 때문에, state값이 변경됨에 따라 재선언된다.
그리고, 이 increaseCount 함수는 함수이자 객체이기때문에 참조값을 비교하는 것으로 useEffect의 재실행 여부가 판단될 것이다.
이를 해결하기 위한 몇 가지 방법이 있다.
1) 이 함수를 그냥 useEffect 내부에 선언해주는 것으로 문제를 해결할 수 있다. 그렇다면 이 함수는 useEffect의 콜백함수 내부에만 접근할 수 있기때문에 의존성 배열에 담길 필요가 없다.
2) 만약 setState 등의 훅을 사용하는 함수가 아니라면, 이 함수는 컴포넌트 외부에서 선언하고 useEffect의 콜백함수 내부에서 호출해 사용하는 것이 좋은 방법이 될 것이다. 이 방법은 1번처럼 useEffect가 재실행될 때 마다 함수 재선언을 방지할 수 있기때문에 용이하다.
3) 그냥 memoization 해서 함수를 기억해 재선언을 방지한다.
모두 좋은 방법이니 상황에 맞게 잘 활용하자.
9. 가끔 useContext를 전역상태관리 도구라고 착각하는 경우가 있다.
useContext는 상태관리와 관련 없다. Props drilling을 해소하기위한 리액트의 기본 제공 훅일 뿐.
이 훅과 전역상태관리 도구들의 용도를 구별하는 것이 매우 중요하다.
PS. 본 내용의 출저는 원티드 프리온보딩 프론트엔드 인턴십의 세션 내용입니다.
'원티드 프리온보딩' 카테고리의 다른 글
1/10 프론트엔드 온보딩 인턴십 내용 정리. (0) 2023.01.10 1/6 프론트엔드 온보딩 인턴십 내용 정리. (0) 2023.01.07 12/20 프론트엔드 온보딩 인턴십 내용 정리. (0) 2022.12.20 [원티드 프리온보딩 프론트엔드 인턴십] 참가 에세이 - 정도영 (0) 2022.12.15 SPA(Single Page Application)로 구성된 웹 앱에서 SSR(Server-side Rendering)이 필요한 이유. (0) 2022.09.24