- styled-jsx는 CSS-in-JS 라이브러리로 css를 캡슐화하고 범위가 지정되게 만들어 구성 요소를 스타일링 할 수 있습니다.
- 한 구성 요소의 스타일링은 다른 구성요소에 영향을 미치지 않도록 하는 특징을 가지고 있습니다.
- next에서 기본으로 제공하므로 설치가 필요 없고 활용이 간편합니다.
> example.jsx
import css from "styled-jsx/css";
const style = css`
h2 {
margin-left: 20px;
}
.user-bio {
margin-top: 12px;
font-style: italic;
}
`;
const username = ({ user }) => {
return (
<>
{user ? (
<div>
<h2>{user.name}</h2>
<p className="user-bio">{user.bio}</p>
</div>
) : (
<div>유저 정보가 없습니다.</div>
)}
<style jsx>{style}</style>
</>
);
};
export default username;
> pages/users/[name].jsx
import css from "styled-jsx/css";
import fetch from "isomorphic-unfetch";
const style = css`
.profile-box {
width: 25%;
max-width: 272px;
margin-right: 26px;
}
.profile-image-wrapper {
width: 100%;
border: 1px solid #e1e4e8;
}
.profile-image-wrapper .profile-image {
display: block;
width: 100%;
}
.profile-username {
margin: 0;
padding-top: 16px;
font-size: 26px;
}
.profile-user-login {
margin: 0;
font-size: 20px;
}
.profile-user-bio {
margin: 0;
padding-top: 16px;
font-size: 14px;
}
`;
const name = ({ user }) => {
if (!user) return null;
return (
<>
<div className="profile-box">
<div className="profile-image-wrapper">
<img
src={user.avatar_url}
alt={`${user.name} 프로필 이미지`}
className="profile-image"
/>
</div>
<h2 className="profile-username">{user.name}</h2>
<p className="profile-user-login">{user.login}</p>
<p className="profile-user-bio">{user.bio}</p>
</div>
<style jsx>{style}</style>
</>
);
};
export const getServerSideProps = async ({ query }) => {
const { name } = query;
try {
const res = await fetch(`https://api.github.com/users/${name}`);
if (res.status === 200) {
const user = await res.json();
return { props: { user } };
}
return { props: {} };
} catch (error) {
console.log(error);
return { props: {} };
}
};
export default name;
react-icons
를 설치하고 필요한 아이콘을 import해서 사용해 보도록 하겠습니다.
우선 view에 해당하는 Profile.jsx
컴포넌트를 생성해 코드를 분리하고 리액트 아이콘을 활용하여 출력을 개선해 보도록 하겠습니다.
> components/Profile.jsx
import css from "styled-jsx/css";
import { GoOrganization, GoLink, GoMail, GoLocation } from "react-icons/go";
const style = css`
.profile-box {
width: 25%;
max-width: 272px;
margin-right: 26px;
}
.profile-image-wrapper {
width: 100%;
border: 1px solid #e1e4e8;
}
.profile-image-wrapper .profile-image {
display: block;
width: 100%;
}
.profile-username {
margin: 0;
padding-top: 16px;
font-size: 26px;
}
.profile-user-login {
margin: 0;
font-size: 20px;
}
.profile-user-bio {
margin: 0;
padding-top: 16px;
font-size: 14px;
}
.profile-user-info {
display: flex;
align-items: center;
margin: 4px 0 0;
}
.profile-user-info-text {
margin-left: 6px;
}
`;
const Profile = ({ user }) => {
if (!user) return null;
return (
<>
<div className="profile-box">
<div className="profile-image-wrapper">
<img
src={user.avatar_url}
alt={`${user.name} 프로필 이미지`}
className="profile-image"
/>
</div>
<h2 className="profile-username">{user.name}</h2>
<p className="profile-user-login">{user.login}</p>
<p className="profile-user-bio">{user.bio}</p>
<p className="prifile-user-info">
<GoOrganization size={16} color="#6a737d" />
<span className="profile-user-info-text">{user.company}</span>
</p>
<p className="prifile-user-info">
<GoLocation size={16} color="#6a737d" />
<span className="profile-user-info-text">{user.location}</span>
</p>
<p className="prifile-user-info">
<GoMail size={16} color="#6a737d" />
<span className="profile-user-info-text">stylenbs@gmail.com</span>
</p>
<p className="prifile-user-info">
<GoLink size={16} color="#6a737d" />
<span className="profile-user-info-text">
devcarrot-skilltree.herokuapp.com
</span>
</p>
</div>
<style jsx>{style}</style>
</>
);
};
export default Profile;
github api로 데이터를 가져와 동적 컴포넌트에 props로 전달하는 로직은 [name].jsx
컴포넌트에 남겨두도록 합니다.
> pages/users/[namx].jsx
import Profile from "../../components/Profile";
import fetch from "isomorphic-unfetch";
const name = ({ user }) => {
if (!user) return null;
return (
<>
<Profile user={user} />
</>
);
};
export const getServerSideProps = async ({ query }) => {
const { name } = query;
try {
const res = await fetch(`https://api.github.com/users/${name}`);
if (res.status === 200) {
const user = await res.json();
return { props: { user } };
}
return { props: {} };
} catch (error) {
console.log(error);
return { props: {} };
}
};
export default name;
빌드 후 localhost에 접속해서 github 아이디를 검색하면 리액트 아이콘과 함께 출력되는 유저 정보를 확인할 수 있습니다.
Profile.jsx
를 만들었던 방식과 동일하게 repositories 리스트를 만들어 봅시다. getServerSideProps
에서 데이터를 요청하고 데이터를 이용하여 view를 만들도록 하겠습니다.
유저의 repositories를 불러오는 github api 경로는 아래와 같습니다.
https://api.github.com/users/${username}/repos?sort=updated&page=1&per_page=10
> pages/users/[name].jsx
import Profile from "../../components/Profile";
import fetch from "isomorphic-unfetch";
import css from "styled-jsx/css";
const style = css`
.user-contents-wrapper {
display: flex;
padding: 20px;
}
.repos-wrapper {
width: 100%;
height: 100vh;
overflow: scroll;
padding: 0px 16px;
}
.repos-header {
padding: 16px 0;
font-size: 14px;
font-weight: 600;
border-bottom: 1px solid #e1e4e8;
}
.repos-count {
display: inline-block;
padding: 2px 5px;
margin-left: 6px;
font-size: 12px;
font-weight: 600;
line-height: 1;
color: #586069;
background-color: rgba(27, 31, 35, 0.08);
border-radius: 20px;
}
.repository-wrapper {
width: 100%;
border-bottom: 1px solid #e1e4e8;
padding: 24px 0;
}
.repository-description {
padding: 12px 0;
}
a {
text-decoration: none;
}
.repository-name {
margin: 0;
color: #0366d6;
font-size: 20px;
display: inline-block;
cursor: pointer;
}
.repository-name:hover {
text-decoration: underline;
}
.repository-description {
margin: 0;
font-size: 14px;
}
.repository-language {
margin: 0;
font-size: 14px;
}
.repository-updated-at {
margin-left: 20px;
}
`;
const name = ({ user, repos }) => {
if (!user) return null;
return (
<div className="user-contents-wrapper">
<Profile user={user} />
<div className="repos-wrapper">
<div className="repos-header">
Repositories
<span className="repos-count">{user.public_repos}</span>
</div>
{user && repos ? (
repos.map((repo) => (
<div className="repository-wrapper" key={repo.id}>
<a
target="_blank"
rel="noreferrer"
href={`https://github.com/${user.login}/${repo.name}`}
>
<h2 className="repository-name">{repo.name}</h2>
</a>
<p className="repository-description">{repo.description}</p>
<p className="repository-language">
{repo.language}
<span className="repository-updated-at"></span>
</p>
</div>
))
) : (
<div>ropository 정보가 없습니다.</div>
)}
</div>
<style jsx>{style}</style>
</div>
);
};
export const getServerSideProps = async ({ query }) => {
const { name } = query;
try {
let user;
let repos;
const userRes = await fetch(`https://api.github.com/users/${name}`);
if (userRes.status === 200) {
user = await userRes.json();
}
const repoRes = await fetch(
`https://api.github.com/users/${name}/repos?sort=updated&page=1&per_page=10`
);
if (repoRes.status === 200) {
repos = await repoRes.json();
}
console.log(repos);
return { props: { user, repos } };
} catch (error) {
console.log(error);
return { props: {} };
}
};
export default name;
a
태그의 ref="noopener noreferrer"
는 보안상의 이유로 위부링크 사용시 추가하는 어트리뷰트값입니다.date-fns는 날짜 및 시간에 관련된 api를 제공하는 라이브러리 입니다. monent가 가장 인기있는 라이브러리지만, 크기 때문에 비교적 가벼운 date-fns를 사용해 봅니다.
$ yarn add date-fns
date-fns를 설치한 뒤 적용해 봅시다.
> pages/users/[name].jsx
...
<p className="repository-language">
{repo.language}
<span className="repository-updated-at">
{formatDistance(
new Date(repo.updated_at),
new Date(),
{ addSuffix: true, }
)
}
</span>
</p>
...
공식문서를 확인하여 더 세부적인 date 설정을 할 수 있으니 참고 바랍니다.
github repositories를 가져오는 api를 보면 query로 page와 per_page를 보내주고 있습니다. 이를 통해 페이지네이션을 구현해 보도록 하겠습니다.
Profile과 마찬가지로 repositories를 위한 컴포넌트를 분리하도록 하겠습니다.
> components/Repositories.jsx
import { useRouter } from "next/router";
import css from "styled-jsx/css";
import Link from "next/link";
import formatDistance from "date-fns/formatDistance";
const style = css`
.repos-wrapper {
width: 100%;
height: 100vh;
overflow: scroll;
padding: 0px 16px;
}
.repos-header {
padding: 16px 0;
font-size: 14px;
font-weight: 600;
border-bottom: 1px solid #e1e4e8;
}
.repos-count {
display: inline-block;
padding: 2px 5px;
margin-left: 6px;
font-size: 12px;
font-weight: 600;
line-height: 1;
color: #586069;
background-color: rgba(27, 31, 35, 0.08);
border-radius: 20px;
}
.repository-wrapper {
width: 100%;
border-bottom: 1px solid #e1e4e8;
padding: 24px 0;
}
.repository-description {
padding: 12px 0;
}
a {
text-decoration: none;
}
.repository-name {
margin: 0;
color: #0366d6;
font-size: 20px;
display: inline-block;
cursor: pointer;
}
.repository-name:hover {
text-decoration: underline;
}
.repository-description {
margin: 0;
font-size: 14px;
}
.repository-language {
margin: 0;
font-size: 14px;
}
.repository-updated-at {
margin-left: 20px;
}
.repository-pagination {
border: 1px solid rgba(27, 31, 35, 0.15);
border-radius: 3px;
width: fit-content;
margin: auto;
margin-top: 20px;
}
.repository-pagination button {
padding: 6px 12px;
font-size: 14px;
border: 0;
color: #0366d6;
background-color: white;
font-weight: bold;
cursor: pointer;
outline: none;
}
.repository-pagination button:first-child {
border-right: 1px solid rgba(27, 31, 35, 0.15);
}
.ropository-pagination button:hover:not([disabled]) {
background-color: #0366d6;
color: white;
}
.repository-pagination button:disabled {
cursor: no-drop;
color: rgba(27, 31, 35, 0.3);
}
`;
const Repositories = ({ user, repos }) => {
const router = useRouter();
const { page = "1" } = router.query;
if (!user || !repos) return null;
return (
<>
<div className="repos-wrapper">
<div className="repos-header">
Repositories
<span className="repos-count">{user.public_repos}</span>
</div>
{repos.map((repo) => (
<div className="repository-wrapper" key={repo.id}>
<a
target="_blank"
rel="noopener noreferrer"
href={`https://github.com/${user.login}/${repo.name}`}
>
<h2 className="repository-name">{repo.name}</h2>
</a>
<p className="repository-description">{repo.description}</p>
<p className="repository-language">
{repo.language}
<span className="repository-updated-at">
{formatDistance(new Date(repo.updated_at), new Date(), {
addSuffix: true,
})}
</span>
</p>
</div>
))}
<div className="repository-pagination">
<Link href={`/users/${user.login}?page=${Number(page) - 1}`}>
<a>
<button type="button" disabled={page && page === "1"}>
Previous
</button>
</a>
</Link>
<Link
href={`/users/${user.login}?page=${!page ? "2" : Number(page) + 1}`}
>
<a>
<button type="button" disabled={repos.length < 10}>
Next
</button>
</a>
</Link>
</div>
</div>
<style jsx>{style}</style>
</>
);
};
export default Repositories;
pagination 로직
작성된 Repositories.jsx
를 import하여 [namx].jsx
를 수정합니다.
import Profile from "../../components/Profile";
import Repositories from "../../components/Repositories";
import fetch from "isomorphic-unfetch";
import css from "styled-jsx/css";
const style = css`
.user-contents-wrapper {
display: flex;
padding: 20px;
}
`;
const name = ({ user, repos }) => {
if (!user) return null;
return (
<div className="user-contents-wrapper">
<Profile user={user} />
<Repositories user={user} repos={repos} />
<style jsx>{style}</style>
</div>
);
};
export const getServerSideProps = async ({ query }) => {
const { name, page } = query;
try {
let user;
let repos;
const userRes = await fetch(`https://api.github.com/users/${name}`);
if (userRes.status === 200) {
user = await userRes.json();
}
const repoRes = await fetch(
`https://api.github.com/users/${name}/repos?sort=updated&page=${page}&per_page=10`
);
if (repoRes.status === 200) {
repos = await repoRes.json();
}
return { props: { user, repos } };
} catch (error) {
console.log(error);
return { props: {} };
}
};
export default name;
라우팅과 서버에서 데이터를 패치하는 방법을 이용하여 pagination을 구현할 수 있습니다.