원티드 프리온보딩

1/20 프론트엔드 온보딩 인턴십 내용 정리.

jdy8739 2023. 1. 23. 10:44

전체 조 과제 피드백 내용

 

1. 프론트 개발자의 마음가짐.

프론트는 항상 유저입장에서 생각을 하면서 코드에 대한 고민을 하여야 한다.

특히, 프론트엔드는 유저와 직접적으로 상호작용하는 최접점, 엣지이기 때문에, 다른 프로그래밍 분야보다 긍정적인 사용자 경험이 효율적인 알고리즘, 비즈니스 로직보다 더 우선시 되어야 하는 경향이 있음다.

물론, 긍정적인 사용자 경험 === 효율적인 로직이라는 명제가 일치하는 경우가 많지만, 그에 벗어나는 예외일 경우 불가피한 상황을 제외하고 긍정적인 사용자 경험을 우선해서 소프트웨어를 만들어야 할 것이다.

{comments.slice(SLICED_POINT, SLICED_END).map((comment) => 
  (<Comment key={comment.id} comment={comment}/>)}

그 예시로 이번 주 과제에서 수행한 페이지네이션 기능을 들 수 있다.

여기서 나는 페이지네이션을 comments 배열에서 slice 함수를 사용해 SLICED_POINT, SLICED_END 변수 값에 따라 페이지에 적합한 정보 comment를 보여주려고 의도했다.

이렇게 되면 페이지 새로고침 시 변수 SLICED_POINT, SLICED_END가 모두 초기화되기 때문에 UI가 새로고침 이전의 정보를 담을 수 없을 뿐만 아니라, 현재 페이지 정보를 공유하고자 하여 url을 다른 브라우저 윈도우에서 요청했을 경우 페이지네이션 정보가 초기화되기 때문에 올바른 정보 공유가 불가능하다.

따라서, 페이지네이션 기능은 최대한 쿼리스트링을 통해 page, offset 정보등을 url에 담아 저장해 주는 것이 좋으며, url도 하나의 정보를 저장하는 공간이기 때문에, 이를 적극적으로 사용하는 것이 권장된다.

즉, 이러한 사용자 반응과 관련된 모든 것들 충실하게 고려하는 것이 프론트엔드 개발자에게 요구되는 가장 중요한 역량이다.

 

2. type 변수 응집도 높이기.

typescript에서 제공하는 모든 type이나 interface 변수들을 하나의 파일에 작성하여 export로 사용하는 경우가 많다.

하지만 하나의 파일에서만 사용될 type과 interface는 그 파일 내부에서 선언하여 사용하는 것이 응집도를 높일 수 있는 사소한 팁이다.

코드리뷰 및 디버깅 시, 그 파일에서만 참조될 요소들을 굳이 다른 파일에 작성하여 시선을 분산시킬 필요가 없기 때문!

즉, 하나의 파일에만 사용되는 요소들은 같은 파일이나 근처 .d.ts 파일에 넣어서 응집도를 높이자.

 

3. 웹 표준, 웹 접근성 항상 고려하기.

const Button = styled.div`
  text-align: right;
  margin: 10px 0;
  & > a {
    margin-right: 10px;
    padding: 0.375rem 0.75rem;
    border-radius: 0.25rem;
    border: 1px solid lightgray;
    cursor: pointer;
  }
`;

다른 조의 과제 코드에서 이러한 styled-component 변수를 선언했다.

여기서 문제가 되는 부분은 div 요소를 clickable한 버튼으로서 사용한 것.

여기서 '웹 접근성'이란 장애가 있어 브라우저 화면을 쉽게 읽을 수 없는 사용자들도 청각적 방법 등을 통해 브라우저의 요소들을 파악할 수 있도록 하는 것인데, 컴퓨터는 div 요소에 click 이벤트리스너를 부착하더라도 이 버튼을 clickable한 요소라고 인식하지 않는다.

따라서, 개발자가 의도한 인터페이스를 온전히 사용자가 사용할 수 없게 되는 것이다.

그렇기 때문에, 우리는 이러한 웹 접근성을 지키면서 html, css를 작성해야 할 것이며 이런 규칙이 잘 적용된 웹 소프트웨어를 보고 '웹 표준이 잘 준수되었다.' 라고 말한다.

웹 표준이 잘 준수된 웹 어플리케이션은 SEO에도 가산점을 부여받아 사용자 노출에 좀 더 유리하기 때문에 이를 잘 준수하자!

 

4. 명령형보다는 선언형 코드를 사용하라.

for (let i = 0; i < numberOfPages; i += 1) {
    const page = i + 1;
    pageArray.push(
      <Page
        key={page}
        active={page === nowPage + 1}
        onClick={() => setNowPage(i)}
      >
        {page}
      </Page>,
    );
  };

  return <PageListStyle>{pageArray}</PageListStyle>;

이 코드 예시는 명령형 코드 작성 방식이다.

게시물 집합의 길이만큼 반복문을 통해 페이지 버튼(JSX.Element)을 배열 안에 집어넣어 페이지네이션 버튼 UI를 구현한다.

이러한 방식은 명령형, 즉 먼저 배열을 생성하고 그 배열에 추가적인 작업을 수행하는 방식을 갖기 때문에 코드의 명확성이 떨어진다.

코드의 길이가 짧아서 망정이지 추가적인 작업이 더 존재한다면, pageArray라는 배열이 어떻게 변화하는지 그 흐름을 계속 추적해야 한다.

const pages = Array.from({ length: numberOfPages }, (_, index) => {
    const pageNum = index + 1;
    return (
      <Page
        key={pageNum}
        active={pageNum === nowPage + 1}
        onClick={() => setNowPage(index)}
      >
        {pageNum}
      </Page>
    );
  });

  return <PageListStyle>{pages}</PageListStyle>;

그러나, 위의 방식으로 구현한다면 배열을 선언 후에 이를 가공하는 것이 아니라 배열을 생성하는 작업에서 모든 작업을 수행하기 때문에, 코드를 읽는 사람이 더욱 이해하기 쉽다.

즉, pages라는 변수가 추적될 필요 없이, 페이지네이션 UI를 구현하는 모든 로직이 수행된 결과물인 배열을 pages라는 변수에 할당해 주는 것이므로 '명령'보다는 '선언'적이라는 특징을 갖는 것이다.

 

5. jsx에서 map 사용 시 key값으로 불변의, 고유의 값을 넣어주자.

일단 SPA 어플리케이션 개발 시, key props는 왜 사용되는 것일까?

그 이유는 map으로 돌려진 요소들 중 하나의 내부에서 상태가 변화되어 이를 처리해야 할 경우, 이 요소가 어느 요소인지 식별하여 로직을 처리하기 위해 고유한 값을 지정해 주어야하기 때문이다.

그러나 이 key 값은 index 처럼 변화하는 값이 아닌 그 요소 고유의 값이어야만 한다.

그 이유는 컴포넌트가 업데이트되는 과정에서 요소들을 다시 렌더링할지 또는, 그대로 사용할지 여부를 이전 가상돔에서 요소의 key 값과 변경 후 가상돔에서 요소의 key값의 일치 여부로 판단하기 때문이다.

따라서, map의 콜백함수가 제공하는 index처럼 각 배열의 요소가 가진 고유 속성과 관련없는 변수들은 key props로 사용되는 것을 지양해야 한다.

그래도 배열의 마지막 요소만 추가되는 비즈니스 로직에서는 각 요소들의 이전의 key 값과 이후의 key 값이 동일하기 때문에 상관없을 수 있다.

그러나, 배열의 맨 앞, 중간에 새로운 요소가 추가되는 로직에서는 특히 index를 key props로 사용할 경우 제대로된 UI의 변경이 일어나지 않는다.

왜냐하면 Index는 컴포넌트 업데이트 전이나 후나, 그대로 0부터 배열의 길이까지 동일한 순서로 전개되기 때문에 배열 순서의 변경을 알아차릴 수 없기 때문이다.

즉, key props는 컴포넌트 업데이트 시, 재조정 과정에 크게 연관이 있기 때문에 그 속성이 유일, 고유해야 한다는 것이다.

 

6. 변수명 잘 신경쓰기.

개발은 혼자하는 것이 아니다.

동료들이 나의 코드를 보고 이 변수, 함수가 무엇이고 어떤 역할을 하는지 파악하기 쉬워야 한다.

또한, 혼자 개발을 한다 하더라도 내가 작성한 코드에서 변수, 함수의 명명이 제대로 되어있지 않다면 불과 며칠후에 내가 작성한 코드를 보더라도 이것이 무엇인지 파악하기 어렵다.

그래서, 한 글자 변수명은 아주 최악이며 info, data, res, result 등의 작명은 용납되지 않는다.

변수가 정확히 무엇을 의미하는지 변수명이 길어지더라도 확실하게 표현해주자.

변수 명명하는 스킬도 할 수록 성장한기 때문에, 나중에는 짧은 변수명으로도 이것이 무언지 명확하게 나타낼 수 있는 경험과 능력이 생길 것이다.

 


수업 내용

 

1. 클로저(closure)란?

굳이 이 포스트에서 클로저의 개념에 대해 깊게 다루진 않을 것이다.

이미 다른 포스트들이나 책에서 그 개념과 내용을 더 잘 알 수 있기 때문이다.

여기서는 React에서 클로저가 어떻게 사용되었는지 수업에서 알게된 내용을 정리하고자 한다.

기본적으로 클로저는 상위 함수의 렉시컬 환경을 기억하는 함수를 말한다.

특정 함수 내부의 실행이 종료되면 그 함수의 실행 컨텍스트 스택이 pop되고 그에 따른 렉시컬 환경도 메모리에서 해제된다.

그러나 상위 함수는 실행이 종료되었지만 그 안에서 선언된 하위 함수가 다른 변수에 할당되어 계속해서 상위 함수에서 선언된 변수를 참조하고 있다면, 그 상위 함수의 렉시컬환경은 계속해서 유지된다.

이런 특성 때문에 클로저는 크게 세가지 특징을 갖는다.

 

1) 상태 기억

const factorializeWithCache = (function (){
  const cache = {};
   
  return function factorialize(num) {
    if (num < 0) throw new Erorr("0보다 큰 숫자를 입력해주세요")
    
    if(cache[num]) return cache[num]
    
    cache[num] = num === 0 ? 1 : num * factorialize(num - 1)
    return cache[num]
  }
})();

위 코드에서 보듯이 factorializeWithCache 함수 안에서 선언된 cache 객체는 반환되는 함수의 실행이 종료되어도 계속해서 그 상태를 기억한다.

즉, 여러번 호출해도 상위 함수에서 선언된 변수의 이전 상태의 값을 기억하고 유지한다.

 

2) 상태 은닉

function makeAddNumFunc(num) {
  const toAdd = num

  return function (num) {
    return num + toAdd
  }
}

const add5 = makeAddNumFunc(5)

상위 함수의 변수는 클로저 내부에서만 참조할 수 있으므로 자연스럽게 접근의 제한이 생긴다.

즉, 캡슐화가 가능하다는 것이다.

때문에, 이를 가지고 좀 더 안정성있는 코드 작성이 가능하다.

 

3) 상태 공유

const makeStudent = (function makeClass(classTeacherName) {
  return function (name) {
    return {
      name: name,
      getTeacherName: () => classTeacherName,
      setTeacherName: name => {
        classTeacherName = name
      },
    }
  }
})("연욱")

const 철수 = makeStudent("철수")
const 영희 = makeStudent("영희")
철수.getTeacherName() // 연욱
영희.getTeacherName() // 연욱

영희.setTeacherName("김봉두")
철수.getTeacherName() // 김봉두

클로저가 참조하는 상위 함수의 변수는 여러 클로저에서 접근해도 같은 값을 공유한다.

즉, 마치 class의 static 변수처럼 활용이 가능한데 어느 클로저에서 해당 변수에 접근하여 수정하면 해당 변수 수정 후 만들어진 클로저가 아닌 이상 같은 값을 공유한다.

 

2. React에서 사용된 클로저 개념은?

놀랍게도 useState, useEffect 훅 모두 사실은 클로저를 사용하고 있다.

컴포넌트가 재렌더링되면 컴포넌트 내부의 코드들이 실행되고 useState 훅이 역시 다시 실행된다.

그러나, 컴포넌트가 몇 번이나 재렌더링 되더라도 그 상태값은 계속해서 이전값을 기억해 참조할 수 있다.

그 이유는 바로 useState에 클로저의 개념이 사용되었기 때문이다.

let hookIndex = 0;
const hooks = [];

const useState = (initialValue) => {
  const state = hooks[hookIndex] || initialValue;
  hooks[hookIndex] = state;

  const setState = (function () {
    const currentHookIndex = hookIndex;

    return (value) => {
      hooks[currentHookIndex] = value;
      hookIndex = 0;
      render();
    };
  })();

  increaseIndex();
  return [state, setState];
};

사실, useState 훅은 위의 작동원리를 따르고 있다.

useState 훅의 상위 함수 내부에서 선언된 변수 hooks는 useState가 기억하는 모든 상태값들의 배열이며, hookIndex는 그 배열 안에서 특정 상태가 몇 번째 순서에 위치하는지를 나타내는 변수이다.

useState가 하나씩 실행될 때마다 hookIndex는 증가하며 hook 배열 내부에 알맞은 상태값들이 다시 할당될 수 있도록 한다.

이 useState 훅들이 여러곳에서 실행되어도 그 모든 상태값들과 그 상태를 관리하는 배열 및 순서는 하나의 렉시컬 환경에서 관리되고 있다는 것이다.

즉, useState 함수는 클로저이다.

따라서, 혹시나 그럴 경우는 없겠지만 컴포넌트 내부에서 조건문을 통해 useState 훅을 사용하는 사람이 있을 수 있다.

이 경우 만약 조건문이 거짓이 되어 useState가 실행되지 않았을 경우, hookIndex의 순서가 엉망이 되어 컴포넌트 재렌더링 시 상태값이 이상하게 할당될 수 있다.

그러므로, useState는 항상 코드의 최상위(top level)에서만 호출되어야 hookIndex의 순서가 꼬이지 않아 올바른 상태값을 전달할 수 있기 때문에 이를 잘 지켜야 한다.

 

const useEffect = (effect, deps) => {
  const prevDeps = hooks[hookIndex];

  const hasChanged = isFirstCall(prevDeps) || isDepsChanged(prevDeps, deps);

  if (hasChanged) {
    effect();
  }

  hooks[hookIndex] = deps;
  increaseIndex();
};

useEffect 역시 상위 함수에서 선언된 변수인 hooks와 hookIndex를 참조하는 클로저로서 구현된다.

hook 배열 내부에 이 useEffect 함수가 가진 의존생 배열들이 저장되며 호출되는 각 useEffect 훅들은 상위 스코프의 hookIndex값에 따라 hooks 배열에서 의존성 요소들을 꺼내어 이전 값과 일치하는지 검사하는 것이다.

결국, 이 모든 원리들은 클로저의 핵심 속성 중 하나인 '상태기억' 이라는 속성에 근본을 두고 있다.

 

주니어 단계에서는 모르고 지나칠 수 있는 내용이지만 React에서 어떻게 클로저가 사용되었으며, 왜 javascript에서 클로저라는 개념이 중요한 것인지 배울 수 있는 너무 좋은 강의였다! :)

 

 

PS. 본 내용의 출저는 원티드 프리온보딩 프론트엔드 인턴십의 세션 내용입니다.