경험하며 배우는 REACT

PARK·2021년 2월 4일
0
post-thumbnail

1.조건부 렌더링

toy project를 진행하고 있었는데

TypeError: Cannot read property 'map' of null

다음과 같은 에러를 생산...

해결하기 위해서 코드를 들여다보니 한 눈에 들어오지 않았다. useEffect에 문제가 있는 줄 알았다.

해결되지 않자 기본을 생각하게 되고 이유를 찾았다.

//정상 코드
function App() {
  const [movies, setMovies] = useState([]);
  const [error, setError] = useState(false);

  useEffect(()=> {
    const axiosMovies = async () => {
      try {
        setError(null);
        const response = await axios.get(FEATURED_API);
        setMovies(response.data.results);
      }catch(e){
        setError(e);
      }
    }
    axiosMovies();
  }, []);

  if(error) return <div>catch error</div>;

  return (
    <div className="App">
      { movies.map(movie => (
        <Movie key= {movie.id}{...movie}
        />
      ))}
    </div>
  );
}
      
//에러 코드
function App() {
  const [movies, setMovies] = useState(null);
  const [error, setError] = useState(false);

  useEffect(()=> {
    const axiosMovies = async () => {
      try {
        setError(null);
        const response = await axios.get(FEATURED_API);
        setMovies(response.data.results);
      }catch(e){
        setError(e);
      }
    }
    axiosMovies();
  }, []);

  if(error) return <div>catch error</div>;

  return (
    <div className="App">
      { movies.map(movie => (
        <Movie key= {movie.id}{...movie}
        />
      ))}
    </div>
  );
}
      
const [movies, setMovies] = useState(null);

위 코드는 초기화를 null로 하느냐 빈배열[]로 하느냐 차이로 실행여부가 갈린다.

이유는 ?

useEffect보다 렌더링이 먼저 되기 때문이다. 그렇기 때문에 null인 상태에서 배열 메소드인 map을 실행시킬 수 없다고 에러가 나오는 것이다. 해결법은 간단하게 null대신 빈 배열 []을 사용하면 된다. 그러면 맨 처음에 잠깐 빈 화면을 띄우다가, 이펙트 함수가 실행되면서 결과물을 띄울 것이다.

원인을 알자 또 다른 해결방법도 생각해봤다.

이렇게 해도 되는구나.

if (!movies) return null;

에러코드에 위 코드를 컴포넌트 안에 넣어주면 []을 사용하지 않아도 가능했다.

{ movies.length > 0 && movies.map(movie => (
        <Movie key= {movie.id}{...movie}
        />
      ))}

조건부 렌더링을 사용해서 movies에 배열이 들어가면 렌더링이 되도록 설정해도 된다.

또 다른 질문이 생기는데, null과 [] 중 선택적으로 사용하는 경우가 있을까 싶다.

2.useEffect

function App() {
  const [movies, setMovies] = useState([]);

    const axiosMovies = async (api) => {
      
        await axios.get(api)
        .then(response=>{
          const fetchData = response.data.results;
          console.log(fetchData); // 1번
          setMovies(fetchData);
          console.log(movies); // 2번
        });
    };
  useEffect(()=> {
    axiosMovies(FEATURED_API);
  
  },[]);
  console.log(movies); //3번
  return (
    <>
      <div className="movie-container">
      { movies.map(movie => (
        <Movie key= {movie.id}{...movie}
        />
      ))}
      </div>
    </>
  );
}
export default App;

코드가 다음과 같을 때 각각 1,2,3번은 어떻게 출력될까?

맨 처음 렌더링되면서 3번이 출력되고 이펙트함수가 실행되
면서 1번, 그리고 state가 업데이트 됐으니 리렌더링되면
서 3번, 마지막으로 2번이 출력된다.

여기서 3번은 결과를 잘 받아와서 배열을 출력시키지만 2번은 여전히 빈 배열을 출력시킨다.

이유는 ?

useEffect의 의존성배열 []은 이펙트 함수를 통해서는 state가 변경될 일이 없다고 명시하는 것과 같다. 그래서 다시는 useEffect의 이펙트함수가 실행되지는 않는다.

처음 실행된 결과는 movies에 []을 집어넣고 이 결과를 끝가지 갖고 있는다.(v1) 그렇기때문에 setMovies가 잘 작동하는 것과 상관없이 v1값인 []을 항상 출력시킨다.

useState의 클로저 영역문제도 있다. 값을 변경하기위해서는 이펙트 함수를 다시 실행시켜서 클로저 함수가 갖고 있는 값을 업데이트 시켜줘야한다.

useEffect(() => {
    // 예를들어
}, [movies]);

이런식으로 작성하면 movies 변경에 따라 이펙트가 실행되고 클로저 영역이 다시 지정되면서 업데이트가 가능할 것이다.


//똑같은 문제를 유발한다!
function App() {
  
  const [movies, setMovies] = useState([]);
  const [fetch, setFetch] = useState(1);

    const axiosMovies = async (api) => {
        
      await axios.get(api)
        .then(response=>{
          const fetchData = response.data.results;
          const mergeData = movies.concat(fetchData);
          setMovies([...mergeData]);
        });
        
    };

  useEffect(()=> {
    axiosMovies(FEATURE_API+fetch);
    window.addEventListener("scroll", onScroll);
    return () => {
      window.removeEventListener("scroll", onScroll);
    };
  },[]);

  const onScroll = () => {
    const scrollHeight = document.documentElement.scrollHeight;
    const scrollTop = document.documentElement.scrollTop;
    const clientHeight = document.documentElement.clientHeight;

    if (scrollTop + clientHeight >= scrollHeight) {
      setFetch(fetch + 1);
      console.log(fetch);
    }
  }
  console.log(fetch);
  return (
    <>
      <div className="movie-container">
      { movies.map(movie => (
        <Movie key= {movie.id}{...movie}
        />
      ))}
      </div>
    </>
  );
}
export default App;

스크롤을 내리면 다음 페이지가 출력되게끔 작성할려고 했으나 당연히 막혔다. 이유는 useEffect에 의존성배열 때문이다.

이유는 ?

역시 위 문제와 같은 이유다

그래서 []은 렌더링 버전마다 최신값임을 보장할 수 없다. 역시 log를 보면 (v1)렌더링에다가 v2, v3 ... 결과를 더하고 있었다.

이렇게 해도 되는구나.

const mergeData = movies.concat(fetchData);
setMovies([...mergeData]);
setMovies(prevMovies => ([...prevMovies, ...pageData]));

위 코드 대신 아래 코드를 사용해도 가능하다. useState의클로저 문제로 effect내부에서 값을 읽을 수는 없지만 콜백함수를 이용해 값을 변경할 수는 있었다.

문제를 해결하며 []는 초기 목록을 불러오는데만 사용하고 다른 경우에는 절대 사용하면 안되겠다고 생각했다.

3.useState

function App() {
  
  const [movies, setMovies] = useState([]);
  const [fetch, setFetch] = useState(1);

    const axiosMovies = async (api) => {
        
      await axios.get(api)
        .then(response=>{
          const fetchData = response.data.results;
          const mergeData = movies.concat(fetchData);
          setMovies([...mergeData]);
        });
        
    };

  useEffect(()=> {
    axiosMovies(FEATURE_API);
    window.addEventListener("scroll", onScroll);
    return () => {
      window.removeEventListener("scroll", onScroll);
    };
  },[]);

  useEffect(()=>{
    axiosMovies(FEATURE_API+fetch);
  },[fetch])

  const onScroll = () => {
    const scrollHeight = document.documentElement.scrollHeight;
    const scrollTop = document.documentElement.scrollTop;
    const clientHeight = document.documentElement.clientHeight;

    if (scrollTop + clientHeight >= scrollHeight) {
      setFetch(fetch + 1);
    }
  }
  console.log(fetch);  //1
  return (
    <>
      <div className="movie-container">
      { movies.map(movie => (
        <Movie key= {movie.id}{...movie}
        />
      ))}
      </div>
    </>
  );
}

새로운 이펙트 함수를 만들며 문제를 해결했지만 살펴볼 부분이 있다.

setFetch(fetch + 1);

위 코드의 문제도 역시 최신 값을 보장할 수 없다는 것이다. 리액트는 state를 비동기적으로 업데이트한다. 그래서 state를 변경하고자 할 때 old value인 상황이 가능하다.
//1 로그를 보면 실제로도 계속 setFetch(1 + 1);을 반복한다.

해결하기 위해서는 함수형 업데이트를 사용하면 된다.
함수형 업데이트는 useCallback과 같은 원리로 최적화할 때 유용하다.

setCount(prevCount => prevCount + 1); 

이렇게 해도 되는구나.

fetch를 useState로 관리하지않고 useRef로 관리해도 좋다.

const fetch = useRef(1);

로 지정하고 조회 및 수정은 fetch.current로 하면된다.
currnet의 뜻과 같이 항상 최신값을 기대할 수 있습니다.

profile
익숙한 것에 작별을 고해야한다

0개의 댓글