git clone -b dev --single-branch https://github.com/rkskekzzz/betti.git Betti
위 명령어를 통해 변경 사항이 적용된 dev 브랜치의 내용을 클론해온다.
UI는 피그마에 잘 정리되어 있기에 내가 할 일은 저 디자인 그대로 구현하는 것.
라우팅을 해줄 페이지는 로그인 페이지, 메인 페이지.
메인 페이지에서도 세부적인 url나 작동에 따라 변경을 해주어야 하지만, 왼쪽의 팀 처럼 그대로 유지해야 할 것도 있다.
메인 페이지는 크게 두 개의 컴포넌트로 구분할 수 있다.
TeamBar
- 내가 속한 팀을 나타냄, 팀을 클릭할 때마다 MainScreen
가 변해야 한다. curPage에 따라 MainScreen
가 변하기 때문에 curPage를 수정하는 콜백함수를 props로 보낸다.MainScreen
- 팀에 해당하는 테스트 목록을 표시, teamData 배열의 curPage를 index로 인식해서 뿌린다.주요 State는 다음과 같다.
TeamBar
에서 팀을 추가하면 갱신된다.TeamBar
에서 팀을 클릭할 때마다 갱신된다.import React from 'react';
import './TeamBar.scss';
const TeamHeader = () => {
const teamOptionHandler = () => {
alert('option event!');
};
return (
<div className="team-header">
Betti!
<img
className="option-icon"
src="assets/option.png"
onClick={teamOptionHandler}
/>
</div>
);
};
const TeamBody = ({ teamData, changePageEvent }) => {
const addTeamHandler = () => {
alert('팀 만드는 모달창');
};
return (
<div className="team-body">
{teamData.map(e => {
return (
<div
className="team"
key={e.index}
onClick={() => changePageEvent(e.index)}
>
{e.name}
</div>
);
})}
<div className="separation">{}</div>
<div className="team team-add" onClick={addTeamHandler}>
+
</div>
</div>
);
};
const TeamBar = ({ teamData, changePageHandler }) => {
return (
<div className="team-bar">
<TeamHeader />
<TeamBody teamData={teamData} changePageEvent={changePageHandler} />
</div>
);
};
export default TeamBar;
TeamHeader
, TeamBody
로 구분된다.
메인 페이지에서 props로 받은 teamData를 map을 통해 div 엘리먼트로 리턴시켜준다.
팀을 클릭할 때마다 MainScreen
은 변해야 한다. changePageEvent는 curPage를 인자에 담긴 값으로 변경한다. 어떤 것을 클릭했는지 분간해야 하기 때문에 같이 넣어준 key값을 보내도록 처리한다.
import { React, useEffect } from 'react';
import './MainScreen.scss';
const MainScreenHeader = ({ teamData, curPage }) => {
return (
<div className="main-screen-header">
<div className="main-screen-team">{teamData[curPage].name}</div>
<div className="main-screen-option">
<img src="assets/teamIcon.png" />
<div>+ {teamData[curPage].test.length}</div>
<img src="assets/etc.png" />
</div>
</div>
);
};
const MainScreenBody = ({ teamData, curPage, curTest, changeTestEvent }) => {
const addTestEvent = () => {};
return (
<div className="main-screen-body">
<div className="main-screen-tests">
{teamData[curPage].test.map((e, i) => {
return (
<div
key={`${curPage}-${i}`}
className="main-screen-test"
onClick={() => changeTestEvent(i)}
>
{e}
</div>
);
})}
<div onClick={addTestEvent} className="main-screen-test add">
+
</div>
</div>
<div className="main-screen-test-info">
{teamData[curPage].test[curTest]}
</div>
</div>
);
};
const MainScreen = ({ teamData, curPage, curTest, changeTestEvent }) => {
useEffect(() => {
console.log(teamData[curPage]);
}, [curPage]);
return (
<div className="main-screen">
<MainScreenHeader teamData={teamData} curPage={curPage} />
<MainScreenBody
teamData={teamData}
curPage={curPage}
curTest={curTest}
changeTestEvent={changeTestEvent}
/>
<div className="separation"></div>
</div>
);
};
export default MainScreen;
MainScreenHeader
, MainScreenBody
로 구분된다.
현재 선택한 팀에 대한 정보를 뿌려야 한다. 부모 컴포넌트에서 state로 관리하는 curPage를 추가로 props에 담아 보낸다. teamData[curPage]로 지금 선택 중인 팀에 대한 정보, 정확히는 팀의 테스트에 대한 정보를 map으로 뿌린다.
각 테스트를 클릭할 때마다 표시하는 테스트도 변해야 한다.
text-align: center;
padding: 10px 0;
html 태그는 브라우저에서 설정한 고유의 값들을 가지고 있기 때문에
* { padding : 0; margin : 0;}
로 초기화한 후 값을 다시 설정하기도 한다.
컴포넌트에 대해서는 다음 링크로.
HTML의 element가 있다면 React에는 Component가 존재한다.
props(input)를 다르게 주면 동일한 UI 형태에 각각 다른 Component(output)를 만들어낸다.
이 컴포넌트를 기본 단위로 해서 기능별로 쪼갠 후 합성할 수 있다는 것이 React의 큰 강점이라고 할 수 있다.
import { React, useEffect } from 'react';
import './MainScreen.scss';
const MainScreen = ({ teamData, curPage }) => {
useEffect(() => {
console.log('Teamdata : ');
console.log(teamData[curPage]);
}, [curPage]);
return (
<div className="main-screen">
<div className="main-screen-header">
<div className="main-screen-team">{teamData[curPage].name}</div>
<div className="main-screen-option">
<img src="assets/teamIcon.png" />
<div>+ {teamData[curPage].test.length}</div>
<img src="assets/etc.png" />
</div>
</div>
<div className="separation"></div>
<div className="main-screen-body">
<div className="main-screen-tests">
{teamData[curPage].test.map((e, i) => {
return (
<div key={`${curPage}-${i}`} className="main-screen-test">
{e}
</div>
);
})}
<div className="main-screen-test add">+</div>
</div>
<div className="main-screen-test-info">시각 정보 제공</div>
</div>
</div>
);
};
export default MainScreen;
위 코드는 MainScreen
이라는 메인 페이지의 본문을 담당하는 컴포넌트에 대한 코드다.
하나의 컴포넌트에 모든 코드를 집어넣어도 구현은 문제없이 되지만, 아무래도 가독성이 떨어진다.
UI만 구현해서 그렇지, 함수들을 작성하면 가독성은 더더욱 떨어질 것이다.
import { React, useEffect } from 'react';
import './MainScreen.scss';
const MainScreenHeader = ({ teamData, curPage }) => {
return (
<div className="main-screen-header">
<div className="main-screen-team">{teamData[curPage].name}</div>
<div className="main-screen-option">
<img src="assets/teamIcon.png" />
<div>+ {teamData[curPage].test.length}</div>
<img src="assets/etc.png" />
</div>
</div>
);
};
const MainScreenBody = ({ teamData, curPage }) => {
return (
<div className="main-screen-body">
<div className="main-screen-tests">
{teamData[curPage].test.map((e, i) => {
return (
<div key={`${curPage}-${i}`} className="main-screen-test">
{e}
</div>
);
})}
<div className="main-screen-test add">+</div>
</div>
<div className="main-screen-test-info">시각 정보 제공</div>
</div>
);
};
const MainScreen = ({ teamData, curPage }) => {
useEffect(() => {
console.log('Teamdata : ');
console.log(teamData[curPage]);
}, [curPage]);
return (
<div className="main-screen">
<MainScreenHeader teamData={teamData} curPage={curPage} />
<MainScreenBody teamData={teamData} curPage={curPage} />
<div className="separation"></div>
</div>
);
};
export default MainScreen;
보통은 각 컴포넌트를 모듈화(파일로 분할)해서 자식 컴포넌트를 import하는 편이지만, 편의를 위해 한 파일에 전부 넣었다.
위의 코드를 기능별, 구조별로 컴포넌트화해서 합성한 코드다. 방금 전의 코드보다 가독성이 훨씬 좋아진 것을 볼 수 있다. 어떤 컴포넌트가 어떤 역할을 하는지도 보다 직관적으로 변했다.
다음과 같은 작업에는 (개인적으로 생각하는) 장점이 하나 더 있다. 크롬 확장 프로그램 중 React Developer Tools를 사용하면 컴포넌트를 기준으로 조회가 가능하다.
컴포넌트 별로 분리해놓으면 조회하는 것이 조금 더 직관적이게 된다.
써놓고 보니 딱히 큰 장점은 아닌 것 같다.
리액트에서 콜백함수를 쓰게 될 일은 많다. 콜백함수를 사용할 때 ()를 붙이게 되면 즉시 실행을 의미하기에 함수 이름만 사용하는데, onClick 이벤트처럼 인자를 담아서 보내야 할 일이 생긴다. 분명 생긴다.
이 때 콜백함수에 인자를 넣어야 하는데, ES6 문법의 화살표 함수를 이용하면 간단하게 해결된다.
const Button = () => {
const [value, setValue] = useState(value);
return (
<>
<div onClick={() => CallbackFunction(value)}></div> // 즉시 실행!
<div onClick={() => CallbackFunction(value)}></div> // 인자 전달!
</>
);
};
경로 설정 시 자꾸만 인식을 못하는 오류 발생. 같은 디렉토리에 넣어 한 번 연결되고 나면 모듈을 이동해도 자동으로 갱신되는 점을 이용해 해결.
jsconfig.js 파일을 이용해 절대경로를 변경해줄 수 있다.
team에 관한 데이터를 컴포넌트가 만들어질 때마다 갱신해주고 TeamBar 컴포넌트에 props으로 보내주어서 map으로 뿌리려고 했는데 에러가 발생했다.
문제의 원인은 props으로 보내준 teamData가 비어있을 때 map을 시도했기 때문이었다. 데이터가 들어오지 않았음에도 렌더링은 진행되기 때문...T^T
&& 연산자를 앞에 붙여줌으로써 데이터가 참일 때만 렌더링하게 수정했더니 해결되었다.
수정
애초에 Main에서 teamData가 존재할 때 props를 보내게끔 && 연산자를 붙여줬다.
메인 페이지에서 State로 관리하고 있는 teamData는 렌더링을 시작할 때 서버로부터 데이터를 받아와야 할 것이다. 서버 연결을 하지 못했기에 useEffect로 비슷한 구현을 했으나 데이터를 채 받아오기도 전에 props로 텅텅 빈 teamData를 뿌리는 이슈가 발생했다.
앞선 map 관련 이슈와 동일한 이유였기에 main에서도 teamData의 값이 존재할 때 컴포넌트를 뿌리도록 해주었다.
에러 메세지로 구글링해보니 100MB 이상의 파일을 push하려고 해서 발생한 오류라고 한다. 어쩌다 들어간 파일인지는 모르겠지만 해당 파일을 삭제한 커밋을 추가해서 push 해도 동일한 오류가 발생했다.
위에서 3번째 커밋에 그 커다란 파일이 들어갔었다. git reset HEAD ^
, git reset HEAD ^~{삭제하고 싶은 커밋 수}
로 로컬의 커밋을 삭제할 수 있다고 한다. 처음 큰 파일이 포함된 커밋까지 삭제해주고 다시 커밋하니 정상 작동했다.
슬랙과 다음 링크의 도움을 받았다.
클러스터 맥에서 brew를 설치해서 npm도 설치한 후 리액트 작업을 하고 있었는데, 갑자기 한 순간에 brew, npm, node가 사라져버렸다... 아무런 전조도 없이 발생한 이슈라서 지금도 원인을 모르겠다. 다시 설치하는 과정을 남기지만 이 글을 다시 보는 일이 없기를 바랄 뿐이다.
둘 중에 되는 방법을 사용.
curl -fsSL https://rawgit.com/kube/42homebrew/master/install.sh | zsh
rm -rf HOME/.brew && git clone --depth=1 [https://github.com/Homebrew/brew](https://github.com/Homebrew/brew) $HOME/.brew && echo 'export PATH=HOME/.brew/bin:$PATH' >> $HOME/.zshrc && source $HOME/.zshrc && brew update
npm(node 포함) 설치
brew install node
onClick : 기능 + Handler?
콜백(props)로 받을 때는 기능 + Event?
기능
기능Header
기능Body
kebab-case
한 컴포넌트를 감싸는 엘리먼트는 컴포넌트와 같은 이름으로
동일한 컴포넌트를 여러개 감싸는 엘리먼트는 복수형.(+s)
main-page에서 파생되었다면 자식 엘리먼트는 main-page- 로 통일.