
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과 [] 중 선택적으로 사용하는 경우가 있을까 싶다.
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내부에서 값을 읽을 수는 없지만 콜백함수를 이용해 값을 변경할 수는 있었다.
문제를 해결하며 []는 초기 목록을 불러오는데만 사용하고 다른 경우에는 절대 사용하면 안되겠다고 생각했다.
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의 뜻과 같이 항상 최신값을 기대할 수 있습니다.