React에서 Closure의 활용

이수빈·2023년 3월 2일
2

React

목록 보기
1/19
  • JavaScript DeepDive를 공부하던때에 클로저라는 개념을 어떻게 코드에서 활용 할 수 있을지 궁금했다.

  • 최근에 React로 구현과제를 하는 중 Closure를 활용한 코드가 있어 이를 기록으로 남기고자 한다.

Closure란?

  • DeepDive에서는 Closure를 외부 함수보다 중첩함수가 더 오래 유지되는 경우, 중첩함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조 할 수 있다. 이러한 중첩 함수를 클로저 라고 정의한다.

  • 코드 예시를 보면 다음과 같다.

const x = 1;

function outer(){
    const x = 10;
    const inner = function(){ console.log(x) };
    return inner; // 렉시컬 환경 > variable 
}
//outer함수 호출 -> inner 반환
//outer함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 pop됨.
//outer함수 생명주기를 마감함. 

const innerFunc = outer(); // function(){console.log(x)}
innerFunc();
// 1이 되는게 아닐까? X
// 10 -> 클로저이기 때문에 외부함수의 변수 참조 가능.
  • 콜스택에서 outer 함수의 실행컨텍스트가 제거되었음에도 inner function이 outer함수를 참조할 수 있는 이유는 무엇일까?

  • 그 이유는 바로 실행컨텍스트 내의 렉시컬 환경이 outer 함수의 렉시컬 환경을 상위스코프로서 저장해놓기 때문이다.

  • outer 함수가 평가되어 함수 객체를 생성할 때 함수 객체의 environment 내부 슬롯에 전역 렉시컬 환경을 참조한다.

  • inner함수는 outer함수의 렉시컬 환경을 상위 스코프로 저장한다.

  • outer 함수가 종료되면 outer함수의 실행 컨텍스트는 컨텍스트 스택에서 제거되지만, outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다. (inner 함수에 의해 참조되고 있기 때문)

  • 가비지 컬렉터는 누군가가 참조하고 있는 메모리공간을 함부로 해제하지 않는다.

렉시컬 환경이란?

  • 렉시컬 환경 : 식별자와 식별자에 바인딩된 값, 상위스코프에 대한 참조를 기록하는 자료구조로 실행 컨텍스트를 구성하는 컴포넌트이다.

  • 렉시컬 환경은 환경레코드와 외부 렉시컬 환경에 대한 참조로 구성된다.

  • 환경레코드(Environment Record) : 스코프에 포함된 식별자를 등록하고 등록된 식별자에 바인딩된 값을 관리하는 저장소

  • 외부 렉시컬 환경에 대한 참조 : 상위 스코프를 가르킨다. 해당 실행 컨텍스트를 생성한 소스코드를 포함하는 상위코드의 렉시컬 환경을 말한다.

코드에서 Closure의 활용

function Question() {
    const [currentIdx, setCurrentIdx] = useState(0);//현재 문제 번호
    const currentTimeRef = useRef(0); //현재 시간 ref
    const [time, setTime] = useState(0); // 현재 시간 state
    const totalTimeRef = useRef(0); // 전체 문제풀이시간


	...  

  const countClosure = () => {
    const countProgress = setInterval(() => {
      currentTimeRef.current += 1;
      setTime(currentTimeRef.current);
      if (currentTimeRef.current === 5) {
        clearInterval(countProgress);
      }
    }, 1000);

    return () => clearInterval(countProgress);
  };
  
  useEffect(() => {
    fetchData();
    const countTotal = setInterval(() => {
      totalTimeRef.current += 1;
    }, 1000);
    return () => clearInterval(countTotal);
  }, []); //전체 시간초 세기
  
   useEffect(() => {
    const cleanUp = countClosure();
    return () => cleanUp();
  }, [currentIdx]);

  
  return (<FlexContainer>
      {isLoading ? (
        <TypoGraphy variant="h1" width={'300px'} height={'0'}>
          Loading...
        </TypoGraphy>
      ) : (
        <>
          {!isDisabled ? (
            <Progress>
              <ProgressBar percent={time * 20} />
            </Progress>: null}
        
        ...//생략
        
      </FlexContainer>)
}
  • setInterval을 이용해 5초를 기준으로 1초마다 줄어드는 progressbar UI를 구현해야했다.

  • 2가지 로직이 존재했는데, 첫번째는 5초가 지났다면 setInterval을 cleanUp 해주는 로직과

  • 5초가 지나지 않았더라도 정답을 맞춰서 다음문제로 넘어갔다면, 이전에 등록되었던 setInterval을 다시 cleanUp 해주는 과정이 필요했다.

  • 첫번째는 setInterval을 호출한 곳에서 5초가 되었다면 Interval 함수를 clean up 해주는 형식으로 구현하였다.

  • 두번째 로직을 구현할때, 처음에는 useEffect안에서 처리를 하려고 했지만, useEffect안에서 countProgress를 호출하면 cleanUp 되는 동시에 하나의 setInterval이 등록이 되고, 문제번호가 변하면 또 다른 setInterval이 등록되는 문제가 발생했다.

  • 그래서 closure을 통해 cleanUp function을 내부함수 형태로 return 하도록 하여 useEffect안의 코드와 스코프를 분리하였고, unmount 될때만 cleanUp이 되도록 progress bar을 구현하였다.


ref)
JS DeepDive - 23강(실행컨텍스트), 24강(클로저)

profile
응애 나 애기 개발자

0개의 댓글