Mistakes | 주니어 리액트 개발자인 내가 실수하고 있었던 것

Positive Ko·2021년 5월 19일
137

React

목록 보기
16/17
post-thumbnail

주니어 리액트 개발자인 내가 실수하고 있었던 것


주니어 개발자 4개월차... 이대로 괜찮은가?
그동안 실수했던 것들을 반성하며 회고하는 시간을 갖습니다...😶
조금은 부끄럽지만 계속 부끄러워지는 것보다 한번 부끄럽고 마는 것이 좋습니다.
파편적인 지식이라 이 또한 틀릴 수도 있습니다.
댓글로 틀린 내용에 대해서 말씀해주시면 5개월차 실수 모음집, 6개월차 실수 모음집을 만드는 데에 도움이 됩니다.



1. 이전 상태를 기반으로 새로운 상태를 세팅하기

새로운 상태는 이전 상태에 기반한 setState를 만들어서 업데이트 해야합니다. 그 이유는 setState이가 비동기적으로 작동해서 상태값을 batch(일정 시간 동안 변화하는 상태를 일괄 처리)로 변경하기 때문입니다.

Bad👎🏻 (This can work yes, but this is not safe.)

const [isState, setState] = useState(false);
const toggleBtn = () => setState(!isState); // (x)

return (
  <Button onClick={toggleBtn} />
)

Good👍🏻

아래와 같이 작성합니다.

const [isState, setState] = useState(false);
const toggleBtn = () => setState(isState => !isState); // (o)

return (
  <Button onClick={toggleBtn} />
)

이전 상태가 주어진 setState가 새로운 상태를 리턴할 수 있도록 바꾸어 줍니다.

💡 setState(!isState)가 아니라 setState(isState => !isState)로 적어야 할까요?
(댓글로 남겨주신 질문에 대한 답을 추가 정리합니다.)

React에서 setState는 state 값을 변경할 때 사용하는 함수입니다.
setState의 동작을 코드로 제가 아는 선에서 대략적으로 나타내보면 다음과 같습니다.

type TCallBack = (prev: object) => object
let state: object;
const setState = (partialState: object | TCallBack) => {
 state = typeof partialState === 'function' ? partialState(state) : partialState;
};

위처럼 setState는 첫 번째 인자를 object나 function으로 받을 수 있는데요, object가 인자일 경우 state = object로 업데이트 됩니다.
여기까진 문제가 없습니다.
하지만 이전의 상태를 기반으로 연속적으로 업데이트 해야할 때에는 (eg. 새로운 상태 = 이전 상태 + 1, setState(!isState)) setState가 비동기적으로 작동하기 때문에 안전하지 않습니다.

const [state, setState] = useState(0);
setState(state + 1);
setState(state + 1);
setState(state + 1);

위와 같은 상황에서 state = 3이 아니라 1이 될 수도 있고.. 랜덤하게 변경이 됩니다.
따라서 setState의 인자로 콜백을 넣어준다면, (eg. setState(prevState => prevState + 1), setState(isState => !isState))
콜스택에 쌓이며 호출된 순서대로 작동하고 최신 상태를 기반으로 실행되기 때문에 안전하게 state를 변경할 수 있습니다.

2. state 얕은 복사

리액트에선 state의 불변성을 지켜주고 setState 함수를 통해 상태 업데이트를 해주어야 합니다.
만약 state의 불변성을 지켜주지 않는다면 컴포넌트 렌더링이 무분별하게 일어날지도 모르고, 컴포넌트를 최적화하기 어려워지겠죠.

const person = {
  name: 'edie',
  age: '77',
  dateJoined: '2021-05-18',
  language: {first: 'javascript', second: 'typescript'}
}

const copiedPerson = {...person};
copiedPerson.language.first = 'python';

console.log(person);
console.log(copiedPerson);

// {
//   name: 'edie',
//   age: '77',
//   dateJoined: '2021-05-18',
//   language: { first: 'python', second: 'typescript' }
// }
// {
//   name: 'edie',
//   age: '77',
//   dateJoined: '2021-05-18',
//   language: { first: 'python', second: 'typescript' }
// }

person의 langauge.first가 python으로 바뀐 것이 보이시죠.
이럴 땐 다음과 같이 바꾸어야 합니다.

const copiedPerson = {...person};
copiedPerson.language = {...person.language, first: 'python'};

console.log(person);
console.log(copiedPerson);

// {
//   name: 'edie',
//   age: '77',
//   dateJoined: '2021-05-18',
//   language: { first: 'javascript', second: 'typescript' }
// }
// {
//   name: 'edie',
//   age: '77',
//   dateJoined: '2021-05-18',
//   language: { first: 'python', second: 'typescript' }
// }

위처럼 참조 데이터 안에 또 다른 참조 데이터가 있을 땐, 그 분기점마다 스프레드 연산자를 사용해야 합니다.
혹은 데이터 구조가 좀 더 복잡해진다면 Immer.jsproduce 함수를 사용해서 상태의 불변성을 지키며 업데이트할 수도 있습니다.

3. ?? Nullish Coalescing과 || OR operator

흔히 value가 undefinednull인 경우를 알기 위해 OR operator를 많이 씁니다.

const value = undefined;
console.log(value || 1); // 1
console.log(value ?? 1); // 1

const value1 = null;
console.log(value1 || 1); // 1
console.log(value1 ?? 1); // 1

이처럼 OR operator(||)는 좌측이 false인 경우, 우측의 값을 리턴합니다.
그런데 undefined도 false이고 null도 false이지만, 0 또한 false이고 '' 또한 false입니다.
따라서 값에 0이나 ''가 들어올 수도 있는 경우 다음처럼 ||와 ??를 구분해서 사용해야 합니다.

const value2 = 0;
console.log(value2 || 1); // 1
console.log(value2 ?? 1); // 0

const value3 = '';
console.log(value3 || null); // null
console.log(value3 ?? null); // ''

이처럼 Nullish coalescing operator(??)는 좌측이 undefined인 경우에만 우측의 값을 리턴하고, 그 외의 경우에는 그대로 리턴합니다.

4. JSON mock data 만들기

JSON Mock data를 만들 때, 일일히 double quote를 치고 있던 제 모습이 기억납니다.
그냥 JSON.stringify()를 쓰면 되는 걸...

const person = {
  name: 'edie',
  age: '77',
  dateJoined: '2021-05-18',
  language: 'javascript'
}

// JSON.stringify(value, replacer, space)
JSON.stringify(person, null, 2);

// '{
//   "name": "edie",
//   "age": "77",
//   "dateJoined": "2021-05-18",
//   "language": "javascript"
// }'

5. 컴포넌트에 boolean props true 표시 X, string props 브라켓 X

컴포넌트에 전달하는 boolean props가 true일 때에는 따로 true를 표시하지 않습니다. string props는 컬리 브라켓을 쓸 필요 없이 double quote로만 전달합니다.

<Components disabled={true} /> // (x)
<Components disabled /> // (o)

<Components title={"제목"} /> // (x)
<Components title={'제목'} /> // (x)
<Components title={`제목`} /> // (x)
<Components title="제목" /> // (o)

자세한 내용은 Airbnb React/JSX Style Guide를 참고합니다.

6. 여러 API 동시에 완료시키기

한 개의 컴포넌트 안에서 여러개의 API를 연결해야 하는 상황이라면 첫 번째의 response와 마지막 열두 번째의 response까지 텀이 발생해서 순차적으로 view가 뜨는 문제가 발생할 수 있습니다.
한 번에 12개의 API를 연결해야하는 컴포넌트에서 모든 view를 한꺼번에 띄울 수 없을까 고민하다가 사용하게 된 것은 Promise.all과 Promise.allSettled이었습니다.

Promise.all, Promise.allSettled 둘 다 모두 array 안에 있는 모든 request가 resolved 되면 결과를 return합니다. 차이점은 Promise.all은 중간에 하나의 request가 rejected 되면 모든 request의 실행이 중단되고 다음의 request는 실행되지 않지만, Promise.allSettled는 하나가 rejected 되더라도 모든 request가 실행되고 결과를 return한다는 점입니다.

따라서 용도와 목적에 따라 all과 allSettled를 구분하여 사용하면 됩니다.

  const axiosBookmark = async () => {
    const sendingUrls = [
      getDongBookmark(),
      getLegalDongBookmark(),
      getKakaoBookmark(),
      getHospitalBookmark(),
    ];

    const response = await Promise.allSettled(sendingUrls);

    console.log(response);
  };

  useEffect(() => {
    axiosBookmark();
  }, []);

다음과 같은 결과가 리턴됩니다.

각각의 request가 fullfilled 혹은 rejected되었는지를 알려주고 그에 따른 결과 data를 얻을 수 있습니다.

7.. 네스팅하지 않고 early return 하기

function PracticeComponent() {
  const [isLoading, setLoading] = useState(true);

  return (
    <>
      {isLoading ? (
        <Spin />
      ) : (
        <PracticeBox>
          <p>Title</p>
          <span>contents</span>
        </PracticeBox>
      )}
    </>
  );
}

export default PracticeComponent;

위처럼 삼항 연산자로 쓰는 경우가 많았습니다. 하지만 다음처럼 네스팅하지 않고 얼리 리턴해버리면 더 코드가 깔끔해 보입니다.

function PracticeComponent() {
  const [isLoading, setLoading] = useState(true);

  if (isLoading) return <Spin />;

  return (
    <PracticeBox>
      <p>Title</p>
      <span>contents</span>
    </PracticeBox>
  );
}

export default PracticeComponent;

마무리

정리한 실수는 여기까지 입니다.

이보다 더 많은 실수들이 있지만 내용을 보다 더 자세하게 담고 싶은 생각에 다른 포스트로 업데이트를 해보려합니다.
읽어주셔서 감사합니다😇

profile
내 이름 고은정, 은을 180deg 돌려 고긍정 🤭

27개의 댓글

comment-user-thumbnail
2021년 5월 25일

좋은 글이네요~

1개의 답글
comment-user-thumbnail
2021년 5월 25일

간만에 보는 긍정코 포스팅 넘나 반가운 것

1개의 답글
comment-user-thumbnail
2021년 5월 26일

잘 배웠습니다.

1개의 답글
comment-user-thumbnail
2021년 5월 27일

와 은정님 글 보고 저도 다시 돌아보는중 🥺 🥺

1개의 답글
comment-user-thumbnail
2021년 5월 27일

안녕하세요. 저는 웹개발자가 아니라 React 와 JavaScript 코드를 처음봐요... 그냥 글이 재미있어서 읽어보면서 Java 언어를 아는 수준에서 예제 코드를 해석해 보려는데 아래 두줄의 차이가 뭔지 도무지 모르겠네요.. 황당한 질문일 수 있는데 .. 아래 두 코드의 동작과 결과의 차이가 무엇인지 혹시 문법적으로 설명해주실 수 있나요? 😅 설명안해주셔도... 괜찮긴 합니다.. 그냥 지나가다가 남겨봅니다..

const toggleBtn = () => setState(!isState); // (x)
const toggleBtn = () => setState(isState => !isState); // (o)

1개의 답글
comment-user-thumbnail
2021년 5월 27일

도지코인 이후로 은정코인..

1개의 답글
comment-user-thumbnail
2021년 5월 28일

깔끔하게 정리하셨네요. 많은 도움될 것 같습니다. 감사합니다.

1개의 답글
comment-user-thumbnail
2021년 5월 28일

감사합니다. 잘배워갑니다.

1개의 답글
comment-user-thumbnail
2021년 5월 31일

잘 읽었습니다.

1개의 답글
comment-user-thumbnail
2021년 5월 31일

ㅋㅋㅋㅋ 또 메인갔넹 잘 배워감니다 ~~

1개의 답글
comment-user-thumbnail
2021년 5월 31일

은정님 항상 레전드네요 👍👍

1개의 답글
comment-user-thumbnail
2021년 6월 22일

안녕하세요! 글 넘넘 잘읽었는데 중간에 2. state 얉은 복사 부분에서 얉은이 아니라 얕은이 맞는것같아요!

1개의 답글
comment-user-thumbnail
2022년 3월 29일

안녕하세요 은정님! 포스트 잘 읽었습니다!
첫번째 1.이전 상태를 기반으로 새로운 상태를 세팅하기 에서

const [isState, setState] = useState(false);
const toggleBtn = () => setState(isState => !isState);

이 라인의 setState(isState => !isState) 이 부분의 isState 는 outer scope 의 isState 는 shadowing 되고 콜백 파라미터 변수명이 쓰인거죠?

밑에 설명하신것 처럼
const [isState, setState] = useState(false);
const toggleBtn = () => setState((prev) => (!prev));
이렇게 파라미터 명을 바꿔서 사용해도 되는거죠?

답글 달기