Slide 1

Slide 1 text

3つのNext.js プロジェクトを 新卒エンジニアと⼀緒に 開発した話 株式会社BeatFit CTO 飯塚 浩也

Slide 2

Slide 2 text

医師 循環器内科、総合診療医と して、⼤学 - 地⽅の病院を 8 年間勤務。 エンジニア 株式会社BeatFit にエンジニ アとして⼊社。 [ ⽇課] Codewars, LeetCode Hack The Box Type Challenge

Slide 3

Slide 3 text

今⽇のお話 3 つのNext.js のプロジェクト どんな、役割分担をしたか? どんな、技術選定をしたか? 新⼊社員研修の参考になれば 🙏

Slide 4

Slide 4 text

サインアップまでの流れ

Slide 5

Slide 5 text

サインアップ (Web) Web (Next.js)

Slide 6

Slide 6 text

1 つめのプロジェクト 実務未経験インターン⽣(23 歳) 期限は1 ヶ⽉間 ⼤型ジム会員のactivate site 2021. 2-3

Slide 7

Slide 7 text

概要 User 登録

Slide 8

Slide 8 text

技術選定 HTTP client: axios css frameWork: tailwind test: jest, Cypress Next.js + Typescript

Slide 9

Slide 9 text

役割分担 → メイン実装 → リファクタリング

Slide 10

Slide 10 text

関⼼の分離 凝集度 状態管理 リファクタリング: 意識したこと

Slide 11

Slide 11 text

初期状態 😓 const Home : NextPage = () => { const [number, setNumber] = useState("")) const [year, selectYear] = useState("1980"); const [month, selectMonth] = useState("1"); const [day, selectDay] = useState("1"); useEffect(() => { ・・・ }, []); ・・・ return(
} } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

Slide 12

Slide 12 text

const Home : NextPage = () => { const [year, selectYear] = useState("1980"); const [month, selectMonth] = useState("1"); const [day, selectDay] = useState("1"); useEffect(() => { ・・・ }, []); ・・・ return(
} } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 } const Container = () => { 1 const [year, selectYear] = useState("1980"); 2 const [month, selectMonth] = useState("1"); 3 const [day, selectDay] = useState("1"); 4 5 6 useEffect(() => { 7 ... 8 }, []); 9 10 return( 11 16 } 17 } 18 19 const Component = ({ 20 year, 21 month, 22 day 23 }: Props) => { 24 return ( 25
26 27
28 ) 29 30 Presentation Domain Separation

Slide 13

Slide 13 text

const Home : NextPage = () => { const [year, selectYear] = useState("1980"); const [month, selectMonth] = useState("1"); const [day, selectDay] = useState("1"); useEffect(() => { ・・・ }, []); ・・・ return(
} } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 } const Container = () => { 1 const [year, selectYear] = useState("1980"); 2 const [month, selectMonth] = useState("1"); 3 const [day, selectDay] = useState("1"); 4 5 6 useEffect(() => { 7 ... 8 }, []); 9 10 return( 11 16 } 17 } 18 19 const Component = ({ 20 year, 21 month, 22 day 23 }: Props) => { 24 return ( 25
26 27
28 ) 29 30 const Container = () => { const [year, selectYear] = useState("1980"); const [month, selectMonth] = useState("1"); const [day, selectDay] = useState("1"); useEffect(() => { ... }, []); return( } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const Component = ({ 20 year, 21 month, 22 day 23 }: Props) => { 24 return ( 25
26 27
28 ) 29 } 30 Presentation Domain Separation

Slide 14

Slide 14 text

const Home : NextPage = () => { const [year, selectYear] = useState("1980"); const [month, selectMonth] = useState("1"); const [day, selectDay] = useState("1"); useEffect(() => { ・・・ }, []); ・・・ return(
} } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 } const Container = () => { 1 const [year, selectYear] = useState("1980"); 2 const [month, selectMonth] = useState("1"); 3 const [day, selectDay] = useState("1"); 4 5 6 useEffect(() => { 7 ... 8 }, []); 9 10 return( 11 16 } 17 } 18 19 const Component = ({ 20 year, 21 month, 22 day 23 }: Props) => { 24 return ( 25
26 27
28 ) 29 30 const Container = () => { const [year, selectYear] = useState("1980"); const [month, selectMonth] = useState("1"); const [day, selectDay] = useState("1"); useEffect(() => { ... }, []); return( } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const Component = ({ 20 year, 21 month, 22 day 23 }: Props) => { 24 return ( 25
26 27
28 ) 29 } 30 const Component = ({ year, month, day }: Props) => { return (
) } const Container = () => { 1 const [year, selectYear] = useState("1980"); 2 const [month, selectMonth] = useState("1"); 3 const [day, selectDay] = useState("1"); 4 5 6 useEffect(() => { 7 ... 8 }, []); 9 10 return( 11 16 } 17 } 18 19 20 21 22 23 24 25 26 27 28 29 30 Presentation Domain Separation

Slide 15

Slide 15 text

const Home : NextPage = () => { const [year, selectYear] = useState("1980"); const [month, selectMonth] = useState("1"); const [day, selectDay] = useState("1"); useEffect(() => { ・・・ }, []); ・・・ return(
} } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 } const Container = () => { 1 const [year, selectYear] = useState("1980"); 2 const [month, selectMonth] = useState("1"); 3 const [day, selectDay] = useState("1"); 4 5 6 useEffect(() => { 7 ... 8 }, []); 9 10 return( 11 16 } 17 } 18 19 const Component = ({ 20 year, 21 month, 22 day 23 }: Props) => { 24 return ( 25
26 27
28 ) 29 30 const Container = () => { const [year, selectYear] = useState("1980"); const [month, selectMonth] = useState("1"); const [day, selectDay] = useState("1"); useEffect(() => { ... }, []); return( } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const Component = ({ 20 year, 21 month, 22 day 23 }: Props) => { 24 return ( 25
26 27
28 ) 29 } 30 const Component = ({ year, month, day }: Props) => { return (
) } const Container = () => { 1 const [year, selectYear] = useState("1980"); 2 const [month, selectMonth] = useState("1"); 3 const [day, selectDay] = useState("1"); 4 5 6 useEffect(() => { 7 ... 8 }, []); 9 10 return( 11 16 } 17 } 18 19 20 21 22 23 24 25 26 27 28 29 30 const Container = () => { const [year, selectYear] = useState("1980"); const [month, selectMonth] = useState("1"); const [day, selectDay] = useState("1"); useEffect(() => { ... }, []); return( } } const Component = ({ year, month, day }: Props) => { return (
) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Presentation Domain Separation

Slide 16

Slide 16 text

低すぎる凝集度・・・ 😰

Slide 17

Slide 17 text

凝集度up 😋 各component が 1 つの機能( 責任) を 持つように分離

Slide 18

Slide 18 text

状態管理は、useContext を使⽤

Slide 19

Slide 19 text

2 つめのプロジェクト 新卒⼊社! 期限は1 ヶ⽉間 ⼤型ジム提携 2021. 4-5

Slide 20

Slide 20 text

役割分担 → メイン実装 → レビュー

Slide 21

Slide 21 text

技術選定 HTTP client: axios + CSS frameWork: tailwind test: jest, Cypress Next.js + Typescript aspida new!

Slide 22

Slide 22 text

従来 const loadProducts = async(): Promise => { const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products } const isArrayOfProducts = (obj: unknown): obj is Product[] => { return Array.isArray(obj) && obj.every(isProduct) } const isProduct = (obj: unknown): obj is Product => { return obj != null && typeof (obj as Product).id === 'string' } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Typescript の ユーザー定義型ガード

Slide 23

Slide 23 text

従来 const loadProducts = async(): Promise => { const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products } const isArrayOfProducts = (obj: unknown): obj is Product[] => { return Array.isArray(obj) && obj.every(isProduct) } const isProduct = (obj: unknown): obj is Product => { return obj != null && typeof (obj as Product).id === 'string' } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() const loadProducts = async(): Promise => { 1 2 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 Typescript の ユーザー定義型ガード

Slide 24

Slide 24 text

従来 const loadProducts = async(): Promise => { const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products } const isArrayOfProducts = (obj: unknown): obj is Product[] => { return Array.isArray(obj) && obj.every(isProduct) } const isProduct = (obj: unknown): obj is Product => { return obj != null && typeof (obj as Product).id === 'string' } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() const loadProducts = async(): Promise => { 1 2 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 5 6 7 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 Typescript の ユーザー定義型ガード

Slide 25

Slide 25 text

従来 const loadProducts = async(): Promise => { const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products } const isArrayOfProducts = (obj: unknown): obj is Product[] => { return Array.isArray(obj) && obj.every(isProduct) } const isProduct = (obj: unknown): obj is Product => { return obj != null && typeof (obj as Product).id === 'string' } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() const loadProducts = async(): Promise => { 1 2 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 5 6 7 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 const isArrayOfProducts = (obj: unknown): obj is Product[] => { return Array.isArray(obj) && obj.every(isProduct) } const isProduct = (obj: unknown): obj is Product => { return obj != null && typeof (obj as Product).id === 'string' } const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 11 12 13 14 15 16 17 Typescript の ユーザー定義型ガード

Slide 26

Slide 26 text

従来 const loadProducts = async(): Promise => { const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products } const isArrayOfProducts = (obj: unknown): obj is Product[] => { return Array.isArray(obj) && obj.every(isProduct) } const isProduct = (obj: unknown): obj is Product => { return obj != null && typeof (obj as Product).id === 'string' } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() const loadProducts = async(): Promise => { 1 2 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 5 6 7 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 const isArrayOfProducts = (obj: unknown): obj is Product[] => { return Array.isArray(obj) && obj.every(isProduct) } const isProduct = (obj: unknown): obj is Product => { return obj != null && typeof (obj as Product).id === 'string' } const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 11 12 13 14 15 16 17 return Array.isArray(obj) && obj.every(isProduct) const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 Typescript の ユーザー定義型ガード

Slide 27

Slide 27 text

従来 const loadProducts = async(): Promise => { const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products } const isArrayOfProducts = (obj: unknown): obj is Product[] => { return Array.isArray(obj) && obj.every(isProduct) } const isProduct = (obj: unknown): obj is Product => { return obj != null && typeof (obj as Product).id === 'string' } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() const loadProducts = async(): Promise => { 1 2 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 5 6 7 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 const isArrayOfProducts = (obj: unknown): obj is Product[] => { return Array.isArray(obj) && obj.every(isProduct) } const isProduct = (obj: unknown): obj is Product => { return obj != null && typeof (obj as Product).id === 'string' } const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 11 12 13 14 15 16 17 return Array.isArray(obj) && obj.every(isProduct) const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 const isArrayOfProducts = (obj: unknown): obj is Product[] => { const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 Typescript の ユーザー定義型ガード

Slide 28

Slide 28 text

従来 const loadProducts = async(): Promise => { const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products } const isArrayOfProducts = (obj: unknown): obj is Product[] => { return Array.isArray(obj) && obj.every(isProduct) } const isProduct = (obj: unknown): obj is Product => { return obj != null && typeof (obj as Product).id === 'string' } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() const loadProducts = async(): Promise => { 1 2 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 5 6 7 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 const isArrayOfProducts = (obj: unknown): obj is Product[] => { return Array.isArray(obj) && obj.every(isProduct) } const isProduct = (obj: unknown): obj is Product => { return obj != null && typeof (obj as Product).id === 'string' } const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 11 12 13 14 15 16 17 return Array.isArray(obj) && obj.every(isProduct) const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 const isArrayOfProducts = (obj: unknown): obj is Product[] => { const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 return products const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 Typescript の ユーザー定義型ガード

Slide 29

Slide 29 text

従来 const loadProducts = async(): Promise => { const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products } const isArrayOfProducts = (obj: unknown): obj is Product[] => { return Array.isArray(obj) && obj.every(isProduct) } const isProduct = (obj: unknown): obj is Product => { return obj != null && typeof (obj as Product).id === 'string' } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() const loadProducts = async(): Promise => { 1 2 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 5 6 7 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 const isArrayOfProducts = (obj: unknown): obj is Product[] => { return Array.isArray(obj) && obj.every(isProduct) } const isProduct = (obj: unknown): obj is Product => { return obj != null && typeof (obj as Product).id === 'string' } const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 11 12 13 14 15 16 17 return Array.isArray(obj) && obj.every(isProduct) const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 const isArrayOfProducts = (obj: unknown): obj is Product[] => { const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 return products 8 } 9 10 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 return products const loadProducts = async(): Promise => { 1 const response = await fetch('https://api.mysite.com/products') 2 const products: unknown = await response.json() 3 4 if (!isArrayOfProducts(products)) { 5 throw new TypeError('Received malformed products API response') 6 } 7 8 } 9 10 const isArrayOfProducts = (obj: unknown): obj is Product[] => { 11 return Array.isArray(obj) && obj.every(isProduct) 12 } 13 14 const isProduct = (obj: unknown): obj is Product => { 15 return obj != null && typeof (obj as Product).id === 'string' 16 } 17 const loadProducts = async(): Promise => { const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products } const isArrayOfProducts = (obj: unknown): obj is Product[] => { return Array.isArray(obj) && obj.every(isProduct) } const isProduct = (obj: unknown): obj is Product => { return obj != null && typeof (obj as Product).id === 'string' } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Typescript の ユーザー定義型ガード

Slide 30

Slide 30 text

With Aspida import aspida from '@aspida/axios' import api from '../api/$api' const client = api(aspida(fetch, { baseURL: 'https://api.mysite.com' })) const loadProducts = async(): Promise => { const res = await client.products.$get() // res: { products: Product[]} return res } 1 2 3 4 5 6 7 8 9 10 11 12 13 export type Methods = { get: { resBody: { products: Product[] } } } 1 2 3 4 5 6 7 api/products/index.ts

Slide 31

Slide 31 text

With Aspida import aspida from '@aspida/axios' import api from '../api/$api' const client = api(aspida(fetch, { baseURL: 'https://api.mysite.com' })) const loadProducts = async(): Promise => { const res = await client.products.$get() // res: { products: Product[]} return res } 1 2 3 4 5 6 7 8 9 10 11 12 13 const client = api(aspida(fetch, { baseURL: 'https://api.mysite.com' })) import aspida from '@aspida/axios' 1 import api from '../api/$api' 2 3 4 5 6 const loadProducts = async(): Promise => { 7 const res = await client.products.$get() 8 // res: { products: Product[]} 9 10 return res 11 } 12 13 export type Methods = { get: { resBody: { products: Product[] } } } 1 2 3 4 5 6 7 api/products/index.ts

Slide 32

Slide 32 text

With Aspida import aspida from '@aspida/axios' import api from '../api/$api' const client = api(aspida(fetch, { baseURL: 'https://api.mysite.com' })) const loadProducts = async(): Promise => { const res = await client.products.$get() // res: { products: Product[]} return res } 1 2 3 4 5 6 7 8 9 10 11 12 13 const client = api(aspida(fetch, { baseURL: 'https://api.mysite.com' })) import aspida from '@aspida/axios' 1 import api from '../api/$api' 2 3 4 5 6 const loadProducts = async(): Promise => { 7 const res = await client.products.$get() 8 // res: { products: Product[]} 9 10 return res 11 } 12 13 const loadProducts = async(): Promise => { const res = await client.products.$get() // res: { products: Product[]} return res } import api from '../api/$api' 2 3 const client = api(aspida(fetch, { baseURL: 'https://api.mysite.com' })) 4 5 6 7 8 9 10 11 12 13 14 export type Methods = { get: { resBody: { products: Product[] } } } 1 2 3 4 5 6 7 api/products/index.ts

Slide 33

Slide 33 text

Code 補完ありがたや〜 😭

Slide 34

Slide 34 text

3 つめのプロジェクト 医学部インターン⽣ join ! 期限は1.5 ヶ⽉間 Web site は 2 つ 実装 2021. 7-8

Slide 35

Slide 35 text

役割分担 → メイン実装、レビュー → メイン実装

Slide 36

Slide 36 text

技術選定 HTTP client: axios + aspida CSS frameWork: tailwind test: jest, Cypress, Mock Service Worker Next.js + Typescript Data fetching: useSWR new! new!

Slide 37

Slide 37 text

Data Fetch Model Frontend Backend Render 毎回server にfetch する必要あり・・・😓 Rerender Fetch

Slide 38

Slide 38 text

Cache Control max-age Client Server Cache max-age: 600

Slide 39

Slide 39 text

< 600s > 600s Client Server Cache Client Server Cache 適切なMax-age の設定が難しい・・・😓

Slide 40

Slide 40 text

Cache Control max-age stale-while-revalidate Client Server max-age: 600 stale-while-revalidate: 100

Slide 41

Slide 41 text

< 600s < 700s Client Server Cache Client Server Cache ① ② ①: stale ( 新鮮でない) ②: revalidate: ( 再検証) ③: cache 更新 ③

Slide 42

Slide 42 text

useSWR fetcher 省略したらどうなる? 🤔

Slide 43

Slide 43 text

ソースコード revalidate されなくなる

Slide 44

Slide 44 text

Client Server Cache ① ② ③ useSWR で、cache を利⽤した 状態管理ができる

Slide 45

Slide 45 text

import useSWR from "swr" export const useStaffNumber = (initialData: string) => { const { data: staffNumber, mutate: setStaffNumber } = useSWR( "staffNumber", null, { initialData } ) return [staffNumber as string, setStaffNumber] as const } 1 2 3 4 5 6 7 8 9 10 11 12 実戦例 import { useStaffNumber } from "../hooks/useSWR" const StaffNumber = () => { const [staffNumber, setStaffNumber] = useStaffNumber("") const handleChange = (event: React.ChangeEvent) => setStaffNumber(event.currentTarget.value) return ( ) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

Slide 46

Slide 46 text

import useSWR from "swr" export const useStaffNumber = (initialData: string) => { const { data: staffNumber, mutate: setStaffNumber } = useSWR( "staffNumber", null, { initialData } ) return [staffNumber as string, setStaffNumber] as const } 1 2 3 4 5 6 7 8 9 10 11 12 "staffNumber", import useSWR from "swr" 1 2 export const useStaffNumber = (initialData: string) => { 3 const { data: staffNumber, mutate: setStaffNumber } = useSWR( 4 5 null, 6 { 7 initialData 8 } 9 ) 10 return [staffNumber as string, setStaffNumber] as const 11 } 12 実戦例 import { useStaffNumber } from "../hooks/useSWR" const StaffNumber = () => { const [staffNumber, setStaffNumber] = useStaffNumber("") const handleChange = (event: React.ChangeEvent) => setStaffNumber(event.currentTarget.value) return ( ) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

Slide 47

Slide 47 text

import useSWR from "swr" export const useStaffNumber = (initialData: string) => { const { data: staffNumber, mutate: setStaffNumber } = useSWR( "staffNumber", null, { initialData } ) return [staffNumber as string, setStaffNumber] as const } 1 2 3 4 5 6 7 8 9 10 11 12 "staffNumber", import useSWR from "swr" 1 2 export const useStaffNumber = (initialData: string) => { 3 const { data: staffNumber, mutate: setStaffNumber } = useSWR( 4 5 null, 6 { 7 initialData 8 } 9 ) 10 return [staffNumber as string, setStaffNumber] as const 11 } 12 null, import useSWR from "swr" 1 2 export const useStaffNumber = (initialData: string) => { 3 const { data: staffNumber, mutate: setStaffNumber } = useSWR( 4 "staffNumber", 5 6 { 7 initialData 8 } 9 ) 10 return [staffNumber as string, setStaffNumber] as const 11 } 12 実戦例 import { useStaffNumber } from "../hooks/useSWR" const StaffNumber = () => { const [staffNumber, setStaffNumber] = useStaffNumber("") const handleChange = (event: React.ChangeEvent) => setStaffNumber(event.currentTarget.value) return ( ) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

Slide 48

Slide 48 text

import useSWR from "swr" export const useStaffNumber = (initialData: string) => { const { data: staffNumber, mutate: setStaffNumber } = useSWR( "staffNumber", null, { initialData } ) return [staffNumber as string, setStaffNumber] as const } 1 2 3 4 5 6 7 8 9 10 11 12 "staffNumber", import useSWR from "swr" 1 2 export const useStaffNumber = (initialData: string) => { 3 const { data: staffNumber, mutate: setStaffNumber } = useSWR( 4 5 null, 6 { 7 initialData 8 } 9 ) 10 return [staffNumber as string, setStaffNumber] as const 11 } 12 null, import useSWR from "swr" 1 2 export const useStaffNumber = (initialData: string) => { 3 const { data: staffNumber, mutate: setStaffNumber } = useSWR( 4 "staffNumber", 5 6 { 7 initialData 8 } 9 ) 10 return [staffNumber as string, setStaffNumber] as const 11 } 12 import useSWR from "swr" export const useStaffNumber = (initialData: string) => { const { data: staffNumber, mutate: setStaffNumber } = useSWR( "staffNumber", null, { initialData } ) return [staffNumber as string, setStaffNumber] as const } 1 2 3 4 5 6 7 8 9 10 11 12 実戦例 import { useStaffNumber } from "../hooks/useSWR" const StaffNumber = () => { const [staffNumber, setStaffNumber] = useStaffNumber("") const handleChange = (event: React.ChangeEvent) => setStaffNumber(event.currentTarget.value) return ( ) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

Slide 49

Slide 49 text

import useSWR from "swr" export const useStaffNumber = (initialData: string) => { const { data: staffNumber, mutate: setStaffNumber } = useSWR( "staffNumber", null, { initialData } ) return [staffNumber as string, setStaffNumber] as const } 1 2 3 4 5 6 7 8 9 10 11 12 "staffNumber", import useSWR from "swr" 1 2 export const useStaffNumber = (initialData: string) => { 3 const { data: staffNumber, mutate: setStaffNumber } = useSWR( 4 5 null, 6 { 7 initialData 8 } 9 ) 10 return [staffNumber as string, setStaffNumber] as const 11 } 12 null, import useSWR from "swr" 1 2 export const useStaffNumber = (initialData: string) => { 3 const { data: staffNumber, mutate: setStaffNumber } = useSWR( 4 "staffNumber", 5 6 { 7 initialData 8 } 9 ) 10 return [staffNumber as string, setStaffNumber] as const 11 } 12 import useSWR from "swr" export const useStaffNumber = (initialData: string) => { const { data: staffNumber, mutate: setStaffNumber } = useSWR( "staffNumber", null, { initialData } ) return [staffNumber as string, setStaffNumber] as const } 1 2 3 4 5 6 7 8 9 10 11 12 実戦例 import { useStaffNumber } from "../hooks/useSWR" const StaffNumber = () => { const [staffNumber, setStaffNumber] = useStaffNumber("") const handleChange = (event: React.ChangeEvent) => setStaffNumber(event.currentTarget.value) return ( ) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const [staffNumber, setStaffNumber] = useStaffNumber("") import { useStaffNumber } from "../hooks/useSWR" 1 2 const StaffNumber = () => { 3 4 5 const handleChange = (event: React.ChangeEvent) => 6 setStaffNumber(event.currentTarget.value) 7 8 return ( 9 15 ) 16 } 17

Slide 50

Slide 50 text

import useSWR from "swr" export const useStaffNumber = (initialData: string) => { const { data: staffNumber, mutate: setStaffNumber } = useSWR( "staffNumber", null, { initialData } ) return [staffNumber as string, setStaffNumber] as const } 1 2 3 4 5 6 7 8 9 10 11 12 "staffNumber", import useSWR from "swr" 1 2 export const useStaffNumber = (initialData: string) => { 3 const { data: staffNumber, mutate: setStaffNumber } = useSWR( 4 5 null, 6 { 7 initialData 8 } 9 ) 10 return [staffNumber as string, setStaffNumber] as const 11 } 12 null, import useSWR from "swr" 1 2 export const useStaffNumber = (initialData: string) => { 3 const { data: staffNumber, mutate: setStaffNumber } = useSWR( 4 "staffNumber", 5 6 { 7 initialData 8 } 9 ) 10 return [staffNumber as string, setStaffNumber] as const 11 } 12 import useSWR from "swr" export const useStaffNumber = (initialData: string) => { const { data: staffNumber, mutate: setStaffNumber } = useSWR( "staffNumber", null, { initialData } ) return [staffNumber as string, setStaffNumber] as const } 1 2 3 4 5 6 7 8 9 10 11 12 実戦例 import { useStaffNumber } from "../hooks/useSWR" const StaffNumber = () => { const [staffNumber, setStaffNumber] = useStaffNumber("") const handleChange = (event: React.ChangeEvent) => setStaffNumber(event.currentTarget.value) return ( ) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const [staffNumber, setStaffNumber] = useStaffNumber("") import { useStaffNumber } from "../hooks/useSWR" 1 2 const StaffNumber = () => { 3 4 5 const handleChange = (event: React.ChangeEvent) => 6 setStaffNumber(event.currentTarget.value) 7 8 return ( 9 15 ) 16 } 17 const [staffNumber, setStaffNumber] = useStaffNumber("") const handleChange = (event: React.ChangeEvent) => setStaffNumber(event.currentTarget.value) import { useStaffNumber } from "../hooks/useSWR" 1 2 const StaffNumber = () => { 3 4 5 6 7 8 return ( 9 15 ) 16 } 17

Slide 51

Slide 51 text

import useSWR from "swr" export const useStaffNumber = (initialData: string) => { const { data: staffNumber, mutate: setStaffNumber } = useSWR( "staffNumber", null, { initialData } ) return [staffNumber as string, setStaffNumber] as const } 1 2 3 4 5 6 7 8 9 10 11 12 "staffNumber", import useSWR from "swr" 1 2 export const useStaffNumber = (initialData: string) => { 3 const { data: staffNumber, mutate: setStaffNumber } = useSWR( 4 5 null, 6 { 7 initialData 8 } 9 ) 10 return [staffNumber as string, setStaffNumber] as const 11 } 12 null, import useSWR from "swr" 1 2 export const useStaffNumber = (initialData: string) => { 3 const { data: staffNumber, mutate: setStaffNumber } = useSWR( 4 "staffNumber", 5 6 { 7 initialData 8 } 9 ) 10 return [staffNumber as string, setStaffNumber] as const 11 } 12 import useSWR from "swr" export const useStaffNumber = (initialData: string) => { const { data: staffNumber, mutate: setStaffNumber } = useSWR( "staffNumber", null, { initialData } ) return [staffNumber as string, setStaffNumber] as const } 1 2 3 4 5 6 7 8 9 10 11 12 実戦例 import { useStaffNumber } from "../hooks/useSWR" const StaffNumber = () => { const [staffNumber, setStaffNumber] = useStaffNumber("") const handleChange = (event: React.ChangeEvent) => setStaffNumber(event.currentTarget.value) return ( ) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const [staffNumber, setStaffNumber] = useStaffNumber("") import { useStaffNumber } from "../hooks/useSWR" 1 2 const StaffNumber = () => { 3 4 5 const handleChange = (event: React.ChangeEvent) => 6 setStaffNumber(event.currentTarget.value) 7 8 return ( 9 15 ) 16 } 17 const [staffNumber, setStaffNumber] = useStaffNumber("") const handleChange = (event: React.ChangeEvent) => setStaffNumber(event.currentTarget.value) import { useStaffNumber } from "../hooks/useSWR" 1 2 const StaffNumber = () => { 3 4 5 6 7 8 return ( 9 15 ) 16 } 17 import { useStaffNumber } from "../hooks/useSWR" const StaffNumber = () => { const [staffNumber, setStaffNumber] = useStaffNumber("") const handleChange = (event: React.ChangeEvent) => setStaffNumber(event.currentTarget.value) return ( ) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

Slide 52

Slide 52 text

useContext と⽐較した感想 参考記事: Shared Hook State with SWR SWR を 状態管理 として活⽤しているよという話

Slide 53

Slide 53 text

まとめ Next.js のプロジェクトを3つ開発しました。 徐々に役割分担をかえてプロジェクトを担当することで 短期間で⼤きく成⻑することができた。 技術選定が毎回変わることで、新たな知⾒を得ることが できた。 成⻑は尊いと感じました・・・!😋