[개발일지] 22년 31주차 - Helog

FeRo 페로·2022년 8월 7일
0

이번 주에는 Velog를 클론코딩 했다. 기간이 짧았기 때문에 모든 기능은 할 수 없었고 회원가입/로그인, 그리고 글쓰기, 댓글쓰기에 대한 부분을 클론하기로 했다. 나는 이번에 메인 페이지와 회원가입/로그인을 담당했었는데 오늘은 이 과정에서 만난 Trouble에 대한 shooting을 정리하면서 한 주를 돌아보도록 하겠다. 즉 trouble shooting과 개발일지를 한 번에 해버리겠다는 말이다ㅎㅎ

스크롤 방향에 따른 반응형 헤더

메인 페이지를 봤을 때 가장 먼저 눈에 띈 부분은 헤더 부분이다. 스크롤이 내려갈 땐 올라가고(숨김), 스크롤이 올라갈 땐 나타나는(노출) 반응형 헤더였다. 구글링을 다양하게 해보니 대부분은 intersection observer를 사용하기 보다는 스크롤 이벤트의 throttling을 통해 해당 부분을 제어했다. 사실 나는 throttling이나 debouncing, intersection observer는 개념만 알고 있었지 한 번도 써본 적은 없었다. 그래서 이건 기회라는 생각에 바로 시도했다.

// Throttle scroll event used timeout!
 const handleScroll = useCallback(() => {
    if (!timer.current) {
     ...
      }, 400);
    }
  }, []);

이렇게 코드가 한 번에 완성되진 않았다. 많은 어려움이 있었는데 throttling을 처음 써봤기 때문에 어떻게 코드로 구현하는지, 그리고 구현된 코드에 어떻게 스크롤 이벤트에 대한 방향을 정보화 할 수 있는지, 그리고 어떻게 그 정보에 대한 반응을 화면에 나타내 줄 수 있는지가 관건이었다.

Throttling을 어떻게 코드로 구현하는지?

throttling을 처음에는 특정 컴포넌트에 Ref를 바인딩하고 그 Ref에 scroll이벤트를 달려고 했다. 그래서 나는 최상위 컴포넌트에 Scroll이벤트를 등록했다. 하지만 Ref에 scroll이벤트를 등록해두니 scroll이벤트가 작동하지 않았다. 이에 대한 해결책으로는 ref가 아니라 window에 이벤트를 등록했다. 이렇게 하니 Scroll이벤트가 정상적으로 작동이 됐다.

사실 이 부분에 있어서는 상황마다 정답이 다르다. 만약에 모달창을 띄었고 그곳에만 스크롤 이벤트를 하게 하기 위해서는 모달을 띄우는 Component에 스크롤 이벤트를 등록하는게 맞다. 하지만 지금처럼 전체 페이지에 들어가는 헤더를 위해서는 window에 이벤트를 등록하는 것이 적합하다.

스크롤 이벤트를 등록했으니 throttling을 구현해 볼 차례다. 이 부분은 여러 reference를 참고했지만 매번 해당 글의 코드가 잘 이해가 안됐다. 하지만 캠프를 함께 하는 익주님에게 소개받은 블로그에서는 jQuery를 사용해서 아주 간단하게 설명을 해주었다. 변수를 하나 설정하고 그 변수에 timeout객체를 할당한다. 그리고 스크롤 이벤트가 발생할 때 마다 해당 변수가 비어있으면 새로운 timeout을 할당해주고 비어있지 않으면 해당 이벤트를 무시하는 방식이었다. 그래서 이 코드에도 해당 방법을 써서 throttling을 구현했다.

어떻게 하면 스크롤 이벤트에 대한 방향을 정보화할 수 있을까?

 const handleScroll = useCallback(() => {
    if (!timer.current) {
      timer.current = setTimeout(() => {
        const { scrollY } = window;
        setScrollY((prevScrollY) => {
          if (prevScrollY > scrollY) {
            // 올라갈 때
            setIsScrollUp(true);
          } else {
            // 내려갈 때
            setIsScrollUp(false);
          }
          return scrollY;
        });
        timer.current = null;
      }, 400);
    }
  }, []);

이 부분은 window에 있는 scrollY에 대한 값을 참고했다. 그래서 state에 해당 값을 할당해주고 이벤트가 발생했을 때 prev값과 비교해서 이전 값 보다 낮으면 isScrollUp state를 true로 설정해주고, 아니면 false로 설정해준다. 이렇게 하니 scroll이벤트에서 발생한 scrollY값을 방향에 대한 데이터로 획득할 수 있었다. 물론 이 부분에서도 무수한 삽질(?)이 있었지만 어떤 방식으로의 삽질인지 상세히 기억이 나지 않을 정도로 긴 시간을 여러 부분에서 삽질을 했다. 그러던 와중에 위 코드로 해결을 했다.

어떻게 정보에 대한 부분을 화면에 나타낼 수 있었을까?

사실 이 부분은 오랫동안 삽질을 하다가 결국 스스로 해결을 못해서 희정님께 여쭤봤었다. 희정님도 여러 고생을 해서 아래 코드를 통해서 해결을 했다. 이 자리를 빌어 항상 많은 도움을 주시는 희정님께 무한한 감사를..

  margin-top: ${(props) => {
    return props.scrollY <= 10 ? "0px" : props.isScrollUp ? "0px" : "-5rem";
  }};

위 코드는 희정님이 주신 답변에 조금의 커스텀을 한 형태인데, header에 대한 styled-components이다. props로 isScrollUp를 전달해주는데 여기서 삼항 연산자를 통해서 true이면 header의 margin-top을 0으로 주고, 내려갈 땐 header의 height 만큼 -5rem 해주었다.
하지만 화면이 렌더링 될 때 가끔 이미 스크롤이 조금 내려간 채로 되는 현상이 있었는데 그렇게 되면 렌더링이 처음 될 때부터 헤더가 위에 올라가서 마치 헤더가 없는 것처럼 보였다. 대략 scrollY가 2정도 내려가는 것이 보통이었다. 그래서 위 코드처럼 scrollY값이 10 밑일 땐 margin-top을 0px로 주어서 그런 경우에도 header가 잘 렌더링 될 수 있도록 해주었다.

React-hook-form

이번 프로젝트에서는 앞서 말한 throttling, debouncing, intersection observer 말고도 react-hook-form이나 react-query, portal을 이용한 모달창 만들기 처럼 아예 써본 적 없는, 혹은 해본 적 없는 도전들을 매우 많이 했다.
모든 도전을 성공하진 못 했지만 그래도 도전을 해봤기 때문에 다음 번 도전에서는 더 잘 해볼 수 있을 것이니 얻은 것이 있다고 할 수 있다. 이어서 이 도전 중 하나인 react-hook-form에 대해서도 이야기를 해보려고 한다.

이 부분은 수영님께 감사를 표현해야 하는데, 수영님의 react코드가 굉장히 진보적이란 생각이 들었다. 그래서 수영님 코드를 보던 중에 react-hook-form에 대해 발견을 하게 되었고 수영님께 여쭤보았을 때 정말 친절히 하나하나 다 알려주셨다. 물론 설명을 듣고 바로 썼던 것은 아니라서 나중에 다시 한 번 docs를 보긴 했지만 한 번 설명을 들었기 때문에 쉽게 이해할 수 있었다. 윤휴먼 최고~👍

export const LoginModal = ({ setIsModalOpen }) => {
  const [isLogin, setIsLogin] = useState(true);
  const [duplicationState, setDuplicationState] = useState(null);

  // useForm 불러오기
  const {
    register,
    handleSubmit,
    getValues,
    formState: { isValid, errors, dirtyFields },
  } = useForm({ mode: "onChange" });
  
  
  ...
  
  // return 부분
  <section>
      <div>
      <h2>로그인</h2>
    <form onSubmit={handleSubmit(handleLogin)}>
      <div>
        <input placeholder="아이디" autoComplete="off" type="text"
          {...register("userId", {
            required: "아이디를 입력하세요",
          })}></input>
        <button className="handle__login__btn" type="submit" disabled{!isValid}>
		    로그인
	  	</button>
        </div>
    	  {errors.userId && (<StyledModalError>{errors.userId.message}  </StyledModalError>)}
	    <input placeholder="비밀번호" type="password" {...register("password", {
        required: "비밀번호를 입력해주세요",})}></input>
    {errors.password && (<StyledModalError>{errors.password.message}</StyledModalError>)}
      </form>
    </div>
    <footer>
      <span>아직 회원이 아니신가요?</span>
      <div onClick={() => setIsLogin(false)}>회원가입</div>
    </footer>
  </section> 

react-hook-form은 form에 대한 관리 및 처리를 매우 간편하게 해주는 라이브러리였다. 가장 인상이 깊었던 점은 helper텍스트에 대한 처리였다. 각각의 input에 여러 조건에 따라, 서로 다른 helper텍스트를 설정해줄 수 있었다. 이는 사용자 경험의 개선이 되는 부분이었기 때문에 굉장히 인상적이었다.

dirtyFields도 매우 인상적이었다. isDirty도 있었지만 reference를 찾아보다가 아래 블로그를 읽고 dirtyFields를 쓰게 됐다. React Hook Form의 isDirty와 dirtyFields를 알아보자
dirtyFields는 form 안에 있는 여러 input 중에서 변화가 일어난 부분을 감지해 객체로 관리한다. 그래서 dirtyFields.key값을 통해서 해당 input의 값이 변화가 되었는지 확인할 수 있었다.

나는 이 부분을 중복확인 버튼의 활성/비활성을 설정에 사용했다. id를 중복확인을 하고 나서 유저가 입력한 id를 변경하고 싶고, 그래서 했다면 disable됐던 중복확인 버튼을 다시 able로 해주어야 했다. 그래서 그때 dirtyFields를 사용했다. 그래서 변경이 감지되면 중복확인 버튼을 다시 활성화 시켜주었다.

Portal로 Modal

모달은 굉장히 흔하게 볼 수 있는 형식이지만 나는 이번 프로젝트를 하면서 처음으로 모달의 개념에 대해서 공부했다. 이렇게 흔하게 쓰이는 친구인데 한 번도 이게 뭔지에 대한 궁금함을 가지지 않았다니! 아직은 프론트 엔드 개발자의 자세가 되지 않았다는 반성을 했다.

그럼 Modal은 어떻게 만들까?

나도 이게 바로 궁금해졌다. 리액트는 root div에 모든 요소들을 렌더링 하는데 그렇다면 또 다른 div를 만들어서 그곳에 Modal을 띄워주어야 하는지에 대해서 궁금해졌다. 이것저것 찾아본 결과 그냥 순수히 React로만 만들 수 있지만 Portal이라는 라이브러리를 이용해서 만드는 경우도 굉장히 많았다. 특히나 Portal은 이전에 root div 말고 다른 div에 컴포넌트를 쉽게 렌더링 해주는 라이브러리로 본 적이 있어서 Portal을 한 번 써봐야겠다는 생각도 하게 되었다.

왜 렌더링이 안되는거야?


Portal을 사용하기 위해서는 가장 먼저 index.html에 root div 외의 다른 div를 만들어 주어야 한다. 근데 여기서 순서가 굉장히 중요한데, 순서가 꼬이면 아예 화면에 모달이 나오지도 않는다. 처음엔 위 사진처럼 생각을 못하고 root가 가장 위에 있는 상태이고 loginmodalbackground가 가장 밑에 있었다.
하지만 아무리 해도 모달이 브라우저에 렌더링 되지 않았다. inspect에서 element를 확인해보니 그곳에도 없었다. 여러가지 방법을 다 해도 안됐는데, 구글링을 해본 결과 모달을 넣어 줄 div를 root 위에 올려야 된다는 말을 보고서 시도해보니 바로 됐다! 그래서 위 순서대로 배치를 했다.


이 부분을 해결하고 나니 그 뒤는 굉장히 쉬웠다. 이렇게 만든 모달을 React.Fragment에 createPortal로 전달해주면 됐다.

배경의 스크롤은 어떻게 막을 수 있을까?

벨로그를 보면 로그인, 회원가입 모달이 렌더링 될 때는 배경의 스크롤이 안됐다. 근데 위에 까지만 코드를 작성한다면 모달 뒤로 신나게 스크롤 되고 있는 창을 볼 수 있다. 구글링을 해보니 아래와 같은 코드로 배경의 스크롤을 막을 수 있었다.

이 부분의 코드는 모달의 백그라운드가 마운트 될 때 설정이 되고 언마운트 될 때 원래대로 돌려주는 방식이다. 먼저 마운트 될 때 position을 fixed와 top을 현재 window의 scrollY 값으로 해서 현재 위치에 고정을 시켜버린다. 그리고 언마운트 될 때 현재 위치(document.body.style.top)을 scrollY로 할당하고 그 위치로 scrollTo를 이용해서 바로 보내준다.

이런 식으로 모달을 켰을 때 백그라운드의 스크롤을 잠글 수 있다.

힘들었던 일주일

이번 주는 굉장히 힘들었다. 캠프 중에 가장 힘들었던 한 주 같다. 지난 주도 힘들었는데, 지난 주는 프로젝트의 실패로 인해 심적으로 힘들었지만, 이번 주는 시간적인 압박 감에서 계속 밤 늦게까지 하다 보니 몸이 너무 힘들었다. 위에 적은 것처럼 짧은 기간에 많은 시도를 하기도 했고 그런 시도들을 하느라 매일 새벽 4,5시까지 하는 건 기본이었는데 주 후반으로 가니까 몸이 정말 힘들었다.
하지만 정말 보람찬 시간이었다. 에자일한 개발! 에자일이란 말만 듣고 그런 식의 개발 방식이 몰입에 좋다는 것만 들었지 이렇게 몸소, 그리고 스스로 에자일함을 경험한 것은 이번이 처음이었다. 스스로 모든 것들을 찾아보고 적용해보고 에러가 나고 구글링 하고를 무수히 반복을 했다. 힘들 수 밖에 없는 개발 방식이지만 그만큼 짧은 시간 안에 많은 것을 해볼 수 있었다. 왜 에자일 에자일 하는지 알게 된 일주일이었다. 두 번 더 에자일 했다가 사람 잡겠네

profile
주먹펴고 일어서서 코딩해

0개의 댓글