위스타그램 (1)

프로젝트 구조

public
├── data
│   └── asideData.json
│   └── feedData.json
├── images
├── index.html
src
├── components 
│   ├── Nav.js
│   ├── Nav.scss
├── pages
│   ├── Login
│   │   └── input.js
│   │   └── Login.js
│   │   └── Login.scss
│   ├── Main
│   │   └── Mainpage
│   │   │    └── Aside
│   │   │    │   └── Footer - Footer.js
│   │   │    │   └── Recommend - Recommend.js
│   │   │    │   └── StoryForm - StoryForm.js
│   │   │    │   └── Aside.js
│   │   │    └── Feed
│   │   │    │   └── Comment - Comment.js
│   │   │    │   └── Feed.js
│   │   │    └── Mainpage.js
│   │   └── Main.js
│   │   └── Main.scss
├── styles
│   ├── common.scss
│   └── reset.css
│   └── variables.css
├── index.js
└── Router.js
└── .prettierrc
└── .eslintrc

실행화면

개발환경

Mac OS 사용
VSC Version: 1.65
react 17.0.2

사용 기술
React

코드 흐름 + 새로 알게 된 부분

로그인 페이지 | 사용자 입력 데이터 저장과 로그인 버튼 활성화 (validation)

const [newInput, setNewInput] = useState({
    id: '',
    password: '',
 });
  • 아이디와 비밀번호 input value 를 저장할 state를 선언 → input이 2개라고해서 state를 따로 선언하는 것이 아니라 객체 형식으로 초기값 지정해서 관리하며 잊지말고 input의 속성값 value에 넣어준다. (input 태그에 속성값 안넣고 이거외않되함ㅋ)
const handleInputValue = e => {
 const { name, value } = e.target;
 setIsValue({ ...isValue, [name]: value });
};
  • <input>에서 onChange 발생 시 handleInput 함수 발생 → 이벤트 타겟인 input을 속성값인 namevalue를 구조분해할당 → 리렌더링 발생시키기 위해 변경함수 setIsValue 에 넣어줌
  • setIsValue({...isValue, [name]: value }); 이 부분이 어려웠는데 spread 문법으로 isValue 객체를 풀어주고, [name]이라는 속성값을 찾아서 e.target.value를 추가해달라는 의미이다.
  • 초기에 이 부분을 아래 코드처럼 구현했는데 리액트 state의 불변성을 위해 원본 statedeep copy하고 복사본을 state에 추가했었다. 이제 굳이 이렇게 할 필요 없이 위처럼 간결한 ES6 문법을 쓰도록 하자!
let arrayCopy = [...isValue]; //원본파일복사 
arrayCopy.push(inputValue);
setIsValue(arrayCopy);

이메일 형식과 비밀번호 자릿수 유효성 검사

const isValid = isValue.id.includes('@') && isValue.password.length > 4;

아래는 리팩토링 전 코드이다.

const checkInput = () => {
    newInput.id.indexOf('@') > 1 && newInput.password.length > 4
      ? setColor('#0095f6')
      : deActivateBtn();
  };
  • 리팩토링 이전에는 삼항연산자로 구현하여 함수를 호출하였다. 하지만 조건식 input.indexOf('@') > 1 && inputPw.length > 4 자체가 Boolean 값의 기준이 될 수 있으므로 굳이 삼항연산자로 구현하지 않고 로그인 buttondisabled={!isValid} 로 간단하게 구현해줄 수 있었다.
  • 또한 함수로 구현할 경우, 패스워드의 길이가 5 이상일 때 버튼이 활성화되지않고 6자리부터 활성화되는 에러를 겪게된다. 그 이유는 useState()가 비동기적이기 때문인데, 아래 코드와 같이 위에서 아래로 순차적으로 코드가 실행되는 것이 아니라, useState()의 변경함수 setInput()이 실행되는 동시에 checkInput()이 실행되어버리기 때문에 벌어진 에러였다.
  • 이러한 문제로 useState 값이 바뀔 때마다 렌더링 하는 방법이 함수형 업데이트인데, setState를 줄 때 위 코드처럼 어떠한 값을 바로 주는 것이 아니라 함수를 통해서 전달하는 방식을 이용하는 것이다. 하지만 더 간단한 불리언 값을 직접 넣어주는 방법으로 해결하였다.
//이전 코드
    const onClick = () => {
        setCount(count+1);
        console.log('click');

        setCount(count+1);
        console.log('click');
    }
    
//함수형 업데이트 코드
    const onClick = () => {
        setCount(count => count+1);
        console.log('click');

        setCount(count => count+1);
        console.log('click');
    }

로그인 페이지 | 로그인 버튼 클릭 시 메인 페이지 이동

  • import { useNavigate } from 'react-router-dom'; 사용
  • const navigate = useNavigate(); 변수 생성해서 불러오기
  • 로그인 버튼에 onClick 이벤트 발생 시 goMain() 함수 발생 시켜 navigate('/suh/main') 로 페이지 라우팅

메인페이지 | Mock data 이용해 여러 피드 구현, 여러 댓글 구현

  • public/dataMock datafeedData.json, asideData.json 생성
[
  {
    "id": 1,
    "userProfileImg": "/images/hyeseong/otter.png",
    "userName": "a.orazy_sudnics",
    "userLocation": "wecode",
    "content": "my name is Moon",
    "thumbnail": "/images/kyungsuh/d.jpeg",
    "likesCount": 15,
    "commentList": [
      {
        "id": 1,
        "userName": "wecode",
        "content": "Welcome to world best coding bootcamp!",
        "isLiked": true
      },
      {
        "id": 2,
        "userName": "wecode2",
        "content": "Hi there.",
        "isLiked": false
      },
      {
        "id": 3,
        "userName": "wecode3",
        "content": "Hey.",
        "isLiked": false
      }
    ]
  },
  {
    "id": 2,
    "userProfileImg": "/images/hyeseong/otter.png",
    "userName": "jaehyuksssss",
    "userLocation": "서울 어딘가",
    "content": "2번 피드",
    "thumbnail": "/images/kyungsuh/c.jpeg",
    "likesCount": 10,
    "commentList": [
      {
        "id": 1,
        "userName": "2-1",
        "content": "Welcome to seoul",
        "isLiked": true
      },
      {
        "id": 2,
        "userName": "2-2",
        "content": "Hi",
        "isLiked": false
      },
      {
        "id": 3,
        "userName": "2-3",
        "content": "Hey there.",
        "isLiked": false
      }
    ]
  },
  {
    "id": 3,
    "userProfileImg": "/images/hyeseong/otter.png",
    "userName": "jeong_hyeon_zzz",
    "userLocation": "LA",
    "content": "3번 피드",
    "thumbnail": "/images/kyungsuh/a.webp",
    "likesCount": 35,
    "commentList": [
      {
        "id": 1,
        "userName": "wecode",
        "content": "best coding bootcamp!",
        "isLiked": true
      },
      {
        "id": 2,
        "userName": "joonsikyang",
        "content": "Welcome",
        "isLiked": false
      },
      {
        "id": 3,
        "userName": "jayPark",
        "content": "Hey. Welcome",
        "isLiked": false
      }
    ]
  },

  {
    "id": 4,
    "userProfileImg": "/images/hyeseong/otter.png",
    "userName": "hyeonegod",
    "userLocation": "서울 어딘가",
    "content": "4번 피드",
    "thumbnail": "/images/kyungsuh/b.webp",
    "likesCount": 52,
    "commentList": [
      {
        "id": 1,
        "userName": "cpu",
        "content": "hello world!",
        "isLiked": true
      },
      {
        "id": 2,
        "userName": "joonsikyang",
        "content": "Hi there.",
        "isLiked": false
      },
      {
        "id": 3,
        "userName": "jayPark",
        "content": "Hey.",
        "isLiked": false
      }
    ]
  },
  {
    "id": 5,
    "userProfileImg": "/images/hyeseong/bonobono.jpeg",
    "userName": "lluxlx_xx",
    "userLocation": "숲속 어딘가",
    "content": "포로리야",
    "thumbnail": "/images/kyungsuh/d.jpeg",
    "likesCount": 100,
    "commentList": [
      {
        "id": 1,
        "userName": "포로리",
        "content": "왜 보노보노야",
        "isLiked": true
      },
      {
        "id": 2,
        "userName": "너부리",
        "content": "나도 간다 ",
        "isLiked": false
      }
    ]
  }
]

아래는 메인 페이지 최상단 <Mainpage/> 코드이다.

function MainPage() {
  const [feedArr, setFeedArr] = useState([]);

  useEffect(() => {
    fetch('/data/kyungsuh/feedData.json')
      .then(res => res.json())
      .then(data => setFeedArr(data));
  });

  return (
    <main className="contMain">
      <section className="mainBox">
        <div>
          {feedArr.map(list => {
            return <Feeds key={list.id} {...list} />;
          })}
        </div>
        <Aside />
      </section>
    </main>
  );
}
  • <Feed/> 컴포넌트를 뿌려주기 위해서 가장 최상단 부모 폴더인 <Mainpage/>에서 state를 관리해준다. mock data를 담아줄 빈 배열 state를 선언하고, fetch 함수로 불러올 데이터 주소를 넣어주는데 fetch 함수는 첫번째 인자로 http 요청을 보낼 API주소, 두번째 인자로 요청을 보낼때의 옵션들을 객체형태로 받는다. http://localhost:3000/data/commentData.json 로 넣어주게 되면 포트번호가 변경될수 있으므로 /data/kyungsuh/feedData.json 이 부분만 넣어주는 것이 유지보수 측면에서 좋다. 이 때 API 주소는 문자열로 입력한다.
  • 그런데 이때 데이터를 요청하는 시점을 특정해야하는데 useEffect 훅을 활용하여 컴포넌트가 렌더링 된 이후 데이터를 요청한다. 요청이 성공적으로 완료되면 setFeedArr 함수를 사용하여 feedArr state 를 응답 받은 값으로 바꿔준다.
  • 요청을 받을 때 json 형태로 바꿔주는데 json을 쓰는 이유는 프론트엔드와 백엔드에서 서로 다른 언어로 통신하므로 우리가 갖는 객체랑 파이썬의 딕셔너리 자료형과 같지가 않다. 그래서 둘 다 같은 형태로 볼 수 있는게 json 형태이다. 다른 언어도 마찬가지로 해당하는 언어를 내 언어에 맞게 컴파일해서 볼 수 있다. 통신할 때는 string 형태로 전달한다. 이 때 fetch 함수는 비동기이므로 then 메서드를 이용해 다음 작업을 진행한다.
{feedArr.map(list => {
     return <Feeds key={list.id} {...list} />;
 })}
  • map으로 배열 feedArr의 요소 하나 하나를 방문하며 그 데이터를 list라는 이름으로 뽑아내서 자식 컴포넌트 Feeds 에 전달한다. 이때 List 데이터의 고유한 값인 idkey 값으로 전달하며 배열의 index를 따로 빼서 전달하지 않는다. 여기서 key를 지정해야하는 이유는 예를들어, 3개의 리스트를 가진 변수를 통해 key가 없이 배열 랜더링을 진행하게 한다면 해당 리스트변수에 1개가 더 추가되는 경우라도 React 는 총 4개를 처음부터 다시 리렌더링 하게 된다. 하지만 key 를 지정한다면 기존의 요소들은 변경되지 않았다는걸 React 에서 자동으로 파악 후 새로생기는 요소에 대해서만 리렌더링을 진행하게 된다. 단순히 key 요소만 추가한것만으로도 더욱 최적화 된 랜더링을 진행할수 있다.

[React] 배열의 index를 key로 쓰면 안되는 이유

느낀 점

Mock data를 받아와 처리하고 반복문으로 돌려 데이터 바인딩 하는 것을 처음 해보았고 아직은 복잡하지 않아 크게 어려움은 없지만 data의 구조가 복잡해질 때를 생각해봐야겠다. 반복으로 돌릴 수 있는 부분을 최대한 반복문을 사용해서 재사용하려고 노력하였다. state를 전달하는 과정에서 자식 컴포넌트에서 선언해놓고 부모에서 필요할 때 아래에서 위로 끌어올려서 쓰려는 실수를 하였고 state는 최대한 최상위 컴포넌트에서 선언하는 것이 좋다는 것을 깨달았다.

2편에서는?

  • 댓글 컴포넌트화 + 피드마다 다른 댓글 데이터 뿌리기(props로 데이터 전달)
  • Aside의 반복되는 부분 모듈화 시켜서 반복문
  • props 구조 분해 할당
  • 코드 리뷰 피드백

추가하고 싶은 기능

  • 모달창
  • 검색 기능
  • 로딩화면 및 에러화면 생성

좋은 웹페이지 즐겨찾기